diff --git a/src/.vuepress/config.js b/src/.vuepress/config.js index 7177cbe1f..18544f4a7 100644 --- a/src/.vuepress/config.js +++ b/src/.vuepress/config.js @@ -337,7 +337,6 @@ module.exports = { '/docs/developers/build/transaction-fees.md', '/docs/developers/build/system-contracts.md', '/docs/developers/build/dev-node.md', - '/docs/developers/build/run-a-node.md', '/docs/developers/build/differences.md', '/docs/developers/build/json-rpc.md', '/docs/developers/build/testing-dapps.md', @@ -355,6 +354,13 @@ module.exports = { ], collapsable: true, }, + { + title: "Operating OP nodes", + children: [ + '/docs/developers/node-oper/which-node.md', + '/docs/developers/node-oper/dockerless.md' + ] + }, '/docs/developers/known-issues.md', { title: "Useful Tools", diff --git a/src/docs/developers/build/run-a-node.md b/src/docs/developers/build/run-a-node.md index d1f160d20..19e29398b 100644 --- a/src/docs/developers/build/run-a-node.md +++ b/src/docs/developers/build/run-a-node.md @@ -1,286 +1,7 @@ ---- -title: Running an OP Mainnet or testnet node -lang: en-US ---- - -If you're looking to build an app on OP Mainnet you'll need access to an OP Mainnet node. You have two options - use a hosted node from providers like Alchemy or run your own. - -## Hosted node providers - -You can get a free, hosted one from [any of these providers](../../useful-tools/providers.md) to get up and building quickly. Of them, [Alchemy](https://www.alchemy.com/optimism) is our preferred node provider, and is used to power our [public endpoint](../../useful-tools/networks.md). - -However, you might be interested in running your very own node. -Here we'll go over the process of running an OP Mainnet or testnet node for yourself. - -## Upgrades - -If you run a node you need to subscribe to [an update feed](../releases.md) (either [the mailing list](https://groups.google.com/a/optimism.io/g/optimism-announce) or [the RSS feed](https://changelog.optimism.io/feed.xml)) to know when to upgrade. -Otherwise, your node will eventually stop working. - -## Configuration choices - -### Hardware requirements - -Replicas need to store the transaction history of OP Mainnet (or the relevant OP testnet) and to run Geth. -They need to be relatively powerful machines (real or virtual). -We recommend at least 16 GB RAM, and an SSD drive with at least 500 GB free (for OP Mainnet). - -### Source of synchronization - - - -[The `op-geth` component](../bedrock/explainer.md#execution-client) synchronizes from both other OP Mainnet (or testnet) nodes (https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/specs/exec-engine.md#happy-path-sync), meaning L2, [and Ethereum (or the appropriate L1 testnet)](https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/specs/exec-engine.md#worst-case-sync) if necessary. - -To synchronize only from L1, you edit the [op-node configuration](https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/specs/rollup-node.md) to set `OP_NODE_P2P_DISABLE` to `true`. - -When you use RPC to get block information (https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/specs/rollup-node.md#l2-output-rpc-method), you can specify one of four options for `blockNumber`: - -- an actual block number -- **pending**: Latest L2 block -- **latest**: Latest block written to L1 -- **finalized**: Latest block fully finalized on L1 (a process that takes 12 minutes with Proof of Stake) - - - -## Docker configuration - -The recommended method to create a replica is to use [Docker](https://www.docker.com/) and the Docker images we provide for [`op-geth`](https://github.com/ethereum-optimism/op-geth/releases/latest) and [`op-node`](https://github.com/ethereum-optimism/optimism/releases/). -For `op-node` you need to scroll down to the latest release that has `op-node`. - -They include all the configuration settings. -This is the recommended method because it is what we for our own systems. -As such, the docker images go through a lot more tests than any other configuration. - -### Configuring and running the node - -Follow [these instructions](https://github.com/smartcontracts/simple-optimism-node) to build and run the node. - - -## Non-docker configuration - -Here are the instructions if you want to build you own read-only replica without relying on our images. -These instructions were generated on an Ubuntu 20.04 box, but they should work with other systems too. - -**Note:** This is *not* the recommended configuration. -While we did QA on these instructions and they work, the QA that the docker images undergo is much more extensive. - - -### Build the Optimism Monorepo - -1. Clone the [Optimism Monorepo](https://github.com/ethereum-optimism/optimism). - - ```bash - cd ~ - git clone https://github.com/ethereum-optimism/optimism.git - ``` - -1. Install required modules. - This is a slow process, while it is running you can already start building `op-geth`, as shown below. - - ```bash - cd optimism - yarn install - ``` - -1. Build the various packages inside of the Optimism Monorepo. - - ```bash - make op-node - yarn build - ``` - -### Build op-geth - -1. Clone [`op-geth`](https://github.com/ethereum-optimism/op-geth): - - ```bash - cd ~ - git clone https://github.com/ethereum-optimism/op-geth.git - ``` - - -1. Build `op-geth`: - - ```bash - cd op-geth - make geth - ``` - - - -### Get the data dir - -The next step is to download the data directory for `op-geth`. - -1. Download the correct data directory snapshot. - - - [OP Mainnet](https://datadirs.optimism.io/mainnet-bedrock.tar.zst) - - [OP Goerli](https://datadirs.optimism.io/goerli-bedrock.tar.zst) - -1. Create the data directory in `op-geth` and fill it. - Note that these directions assume the data directory snapshot is at `~`, the home directory. Modify if needed. - - ```sh - cd ~/op-geth - mkdir datadir - cd datadir - tar xvf ~/*bedrock.tar - ``` - -1. Create a shared secret with `op-node`: - - ```sh - cd ~/op-geth - openssl rand -hex 32 > jwt.txt - cp jwt.txt ~/optimism/op-node - ``` - -### Scripts to start the different components - -#### `op-geth` - -This is the script for OP Goerli. -For OP Mainnet (or other OP networks in the future, [get the sequencer URL here](../../useful-tools/networks.md)). - -``` -#! /usr/bin/bash - -SEQUENCER_URL=https://goerli-sequencer.optimism.io/ - -cd ~/op-geth - -./build/bin/geth \ - --ws \ - --ws.port=8546 \ - --ws.addr=0.0.0.0 \ - --ws.origins="*" \ - --http \ - --http.port=8545 \ - --http.addr=0.0.0.0 \ - --http.vhosts="*" \ - --http.corsdomain="*" \ - --authrpc.addr=localhost \ - --authrpc.jwtsecret=./jwt.txt \ - --authrpc.port=8551 \ - --authrpc.vhosts="*" \ - --datadir=/data \ - --verbosity=3 \ - --rollup.sequencerhttp=$SEQUENCER_URL \ - --nodiscover \ - --syncmode=full \ - --maxpeers=0 \ - --datadir ./datadir \ - --snapshot=false -``` - - -::: info Snapshots - -For the initial synchronization it's a good idea to disable snapshots (`--snapshot=false`) to speed it up. -Later, for regular usage, you can remove that option to improve geth database integrity. - -::: - -#### `op-node` - -- Change `<< URL to L1 >>` to a service provider's URL for the L1 network (either L1 Ethereum or Goerli). -- Set `L1KIND` to the network provider you are using (alchemy, infura, etc.). -- Set `NET` to either `goerli` or `mainnet`. - - -``` -#! /usr/bin/bash - -L1URL= << URL to L1 >> -L1KIND=alchemy -NET=goerli - -cd ~/optimism/op-node - -./bin/op-node \ - --l1=$L1UL \ - --l1.rpckind=$L1KIND \ - --l2=http://localhost:8551 \ - --l2.jwt-secret=./jwt.txt \ - --network=$NET \ - --rpc.addr=0.0.0.0 \ - --rpc.port=8547 - -``` - - - - -### The initial synchornization - -The datadir provided by Optimism is not updated continuously, so before you can use the node you need a to synchronize it. - -During that process you get log messages from `op-node`, and nothing else appears to happen. - -``` -INFO [06-26|13:31:20.389] Advancing bq origin origin=17171d..1bc69b:8300332 originBehind=false -``` - -That is normal - it means that `op-node` is looking for a location in the batch queue. -After a few minutes it finds it, and then it can start synchronizing. - -While it is synchronizing, you can expect log messages such as these from `op-node`: - -``` -INFO [06-26|14:00:59.460] Sync progress reason="processed safe block derived from L1" l2_finalized=ef93e6..e0f367:4067805 l2_safe=7fe3f6..900127:4068014 l2_unsafe=7fe3f6..900127:4068014 l2_time=1,673,564,096 l1_derived=6079cd..be4231:8301091 -INFO [06-26|14:00:59.460] Found next batch epoch=8e8a03..11a6de:8301087 batch_epoch=8301087 batch_timestamp=1,673,564,098 -INFO [06-26|14:00:59.461] generated attributes in payload queue txs=1 timestamp=1,673,564,098 -INFO [06-26|14:00:59.463] inserted block hash=e80dc4..72a759 number=4,068,015 state_root=660ced..043025 timestamp=1,673,564,098 parent=7fe3f6..900127 prev_randao=78e43d..36f07a fee_recipient=0x4200000000000000000000000000000000000011 txs=1 update_safe=true -``` - -And log messages such as these from `op-geth`: - -``` -INFO [06-26|14:02:12.974] Imported new potential chain segment number=4,068,194 hash=a334a0..609a83 blocks=1 txs=1 mgas=0.000 elapsed=1.482ms mgasps=0.000 age=5mo2w20h dirty=2.31MiB -INFO [06-26|14:02:12.976] Chain head was updated number=4,068,194 hash=a334a0..609a83 root=e80f5e..dd06f9 elapsed="188.373µs" age=5mo2w20h -INFO [06-26|14:02:12.982] Starting work on payload id=0x5542117d680dbd4e -``` - -#### How long will the synchronization take? - -To estimate how long the synchronization will take, you need to first find out how many blocks you synchronize in a minute. - -You can use this script, which uses [Foundry](https://book.getfoundry.sh/). and the UNIX Note that this script is for OP Goerli. -For OP Mainnet substitute `https://mainnet.optimism.io` - -```sh -#! /usr/bin/bash - -export ETH_RPC_URL=http://localhost:8545 -T0=`cast block latest number` ; sleep 60 ; T1=`cast block latest number` -PER_MIN=`expr $T1 - $T0` -echo Blocks per minute: $PER_MIN - - -if [ $PER_MIN -eq 0 ]; then - echo Not synching - exit; -fi - -# During that minute the head of the chain progressed by thirty blocks -PROGRESS_PER_MIN=`expr $PER_MIN - 30` -echo Progress per minute: $PROGRESS_PER_MIN - - -# How many more blocks do we need? -HEAD=`cast block --rpc-url https://goerli.optimism.io latest number` -BEHIND=`expr $HEAD - $T1` -MINUTES=`expr $BEHIND / $PROGRESS_PER_MIN` -HOURS=`expr $MINUTES / 60` -echo Hours until sync completed: $HOURS - -if [ $HOURS -gt 24 ] ; then - DAYS=`expr $HOURS / 24` - echo Days until sync complete: $DAYS -fi -``` - - -### Operations - -It is best to start `op-geth` first and shut it down last. + diff --git a/src/docs/developers/node-oper/README.md b/src/docs/developers/node-oper/README.md new file mode 120000 index 000000000..6b5ca8aea --- /dev/null +++ b/src/docs/developers/node-oper/README.md @@ -0,0 +1 @@ +which-node.md \ No newline at end of file diff --git a/src/docs/developers/node-oper/dockerless.md b/src/docs/developers/node-oper/dockerless.md new file mode 100644 index 000000000..3fcb969c7 --- /dev/null +++ b/src/docs/developers/node-oper/dockerless.md @@ -0,0 +1,273 @@ +--- +title: Running a node without Docker +lang: en-US +--- + + +Here are the instructions if you want to build you own OP Mainnet, or OP Goerli, replica without relying on our images. +These instructions were generated on an Ubuntu 20.04 LTS box, but they should work with other systems too. + +**Note:** This is *not* the recommended configuration. +While we did QA on these instructions and they work, the QA that the docker images undergo is much more extensive. + +## Prerequisites + +You’ll need the following software installed to follow this tutorial: + +- [Git](https://git-scm.com/) +- [Go](https://go.dev/) +- [Node](https://nodejs.org/en/) +- [Pnpm](https://classic.yarnpkg.com/lang/en/docs/install/) +- [Foundry](https://github.com/foundry-rs/foundry#installation) +- [Make](https://linux.die.net/man/1/make) +- [jq](https://github.com/jqlang/jq) +- [direnv](https://direnv.net) +- [zstd](http://facebook.github.io/zstd/) + +This tutorial was checked on: + +| Software | Version | Installation command(s) | +| -------- | ---------- | - | +| Ubuntu | 20.04 LTS | | +| git, curl, jq, make, and zstd | OS default | `sudo apt update`
`sudo apt install -y git curl make jq zstd`| +| Go | 1.20 | `wget https://go.dev/dl/go1.20.linux-amd64.tar.gz`
`tar xvzf go1.20.linux-amd64.tar.gz`
`sudo cp go/bin/go /usr/bin/go`
`sudo mv go /usr/lib`
`echo export GOROOT=/usr/lib/go >> ~/.bashrc` +| Node | 16.19.0 | `curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -`
`sudo apt-get install -y nodejs` +| pnpm | 8.5.6 | `sudo npm install -g pnpm` +| yarn | 1.22.19 | `sudo npm install -g yarn` +| Foundry | 0.2.0 | `curl -L https://foundry.paradigm.xyz | bash`
`. ~/.bashrc`
`foundryup` + + +## Build the Optimism Monorepo + +1. Clone the [Optimism Monorepo](https://github.com/ethereum-optimism/optimism). + + ```bash + cd ~ + git clone https://github.com/ethereum-optimism/optimism.git + ``` + +1. Install required modules. + This is a slow process, while it is running you can already start building `op-geth`, as shown below. + + ```bash + cd optimism + pnpm install + ``` + +1. Build the necessary packages inside of the Optimism Monorepo. + + ```bash + make op-node + pnpm build + ``` + +## Build op-geth + +1. Clone [`op-geth`](https://github.com/ethereum-optimism/op-geth): + + ```bash + cd ~ + git clone https://github.com/ethereum-optimism/op-geth.git + ``` + + +1. Build `op-geth`: + + ```bash + cd op-geth + make geth + ``` + + + +## Get the data dir + +The next step is to download the data directory for `op-geth`. + +1. Download the correct data directory snapshot. + + - [OP Mainnet](https://datadirs.optimism.io/mainnet-bedrock.tar.zst) + - [OP Goerli](https://datadirs.optimism.io/goerli-bedrock.tar.zst) + +1. Create the data directory in `op-geth` and fill it. + Note that these directions assume the data directory snapshot is at `~`, the home directory. Modify if needed. + + ```sh + cd ~/op-geth + mkdir datadir + cd datadir + tar xvf ~/*bedrock.tar + ``` + +1. Create a shared secret with `op-node`: + + ```sh + cd ~/op-geth + openssl rand -hex 32 > jwt.txt + cp jwt.txt ~/optimism/op-node + ``` + +## Scripts to start the different components + +### `op-geth` + +This is the script for OP Goerli. +For OP Mainnet (or other OP networks in the future), [get the sequencer URL here](../../useful-tools/networks.md)). + +``` +#! /usr/bin/bash + +SEQUENCER_URL=https://goerli-sequencer.optimism.io/ + +cd ~/op-geth + +./build/bin/geth \ + --ws \ + --ws.port=8546 \ + --ws.addr=0.0.0.0 \ + --ws.origins="*" \ + --http \ + --http.port=8545 \ + --http.addr=0.0.0.0 \ + --http.vhosts="*" \ + --http.corsdomain="*" \ + --authrpc.addr=localhost \ + --authrpc.jwtsecret=./jwt.txt \ + --authrpc.port=8551 \ + --authrpc.vhosts="*" \ + --datadir=/data \ + --verbosity=3 \ + --rollup.sequencerhttp=$SEQUENCER_URL \ + --nodiscover \ + --syncmode=full \ + --maxpeers=0 \ + --datadir ./datadir \ + --snapshot=false +``` + + +::: info Snapshots + +For the initial synchronization it's a good idea to disable snapshots (`--snapshot=false`) to speed it up. +Later, for regular usage, you can remove that option to improve geth database integrity. + +::: + +### `op-node` + +- Change `<< URL to L1 >>` to a service provider's URL for the L1 network (either L1 Ethereum or Goerli). +- Set `L1KIND` to the network provider you are using (alchemy, infura, etc.). +- Set `NET` to either `goerli` or `mainnet`. + + +``` +#! /usr/bin/bash + +L1URL= << URL to L1 >> +L1KIND=alchemy +NET=goerli + +cd ~/optimism/op-node + +./bin/op-node \ + --l1=$L1URL \ + --l1.rpckind=$L1KIND \ + --l2=http://localhost:8551 \ + --l2.jwt-secret=./jwt.txt \ + --network=$NET \ + --rpc.addr=0.0.0.0 \ + --rpc.port=8547 + +``` + + + + +## The initial synchornization + +The datadir provided by Optimism is not updated continuously, so before you can use the node you need a to synchronize it. + +During that process you get log messages from `op-node`, and nothing else appears to happen. + +``` +INFO [06-26|13:31:20.389] Advancing bq origin origin=17171d..1bc69b:8300332 originBehind=false +``` + +That is normal - it means that `op-node` is looking for a location in the batch queue. +After a few minutes it finds it, and then it can start synchronizing. + +While it is synchronizing, you can expect log messages such as these from `op-node`: + +``` +INFO [06-26|14:00:59.460] Sync progress reason="processed safe block derived from L1" l2_finalized=ef93e6..e0f367:4067805 l2_safe=7fe3f6..900127:4068014 l2_unsafe=7fe3f6..900127:4068014 l2_time=1,673,564,096 l1_derived=6079cd..be4231:8301091 +INFO [06-26|14:00:59.460] Found next batch epoch=8e8a03..11a6de:8301087 batch_epoch=8301087 batch_timestamp=1,673,564,098 +INFO [06-26|14:00:59.461] generated attributes in payload queue txs=1 timestamp=1,673,564,098 +INFO [06-26|14:00:59.463] inserted block hash=e80dc4..72a759 number=4,068,015 state_root=660ced..043025 timestamp=1,673,564,098 parent=7fe3f6..900127 prev_randao=78e43d..36f07a fee_recipient=0x4200000000000000000000000000000000000011 txs=1 update_safe=true +``` + +And log messages such as these from `op-geth`: + +``` +INFO [06-26|14:02:12.974] Imported new potential chain segment number=4,068,194 hash=a334a0..609a83 blocks=1 txs=1 mgas=0.000 elapsed=1.482ms mgasps=0.000 age=5mo2w20h dirty=2.31MiB +INFO [06-26|14:02:12.976] Chain head was updated number=4,068,194 hash=a334a0..609a83 root=e80f5e..dd06f9 elapsed="188.373µs" age=5mo2w20h +INFO [06-26|14:02:12.982] Starting work on payload id=0x5542117d680dbd4e +``` + +### How long will the synchronization take? + +To estimate how long the synchronization will take, you need to first find out how many blocks you synchronize in a minute. + +You can use this script, which uses [Foundry](https://book.getfoundry.sh/). + + + +```sh +#! /usr/bin/bash + +export ETH_RPC_URL=http://localhost:8545 +CHAIN_ID=`cast chain-id` +echo Chain ID: $CHAIN_ID +echo Please wait + +if [ $CHAIN_ID -eq 10 ]; then + L2_URL=https://mainnet.optimism.io +fi + + +if [ $CHAIN_ID -eq 420 ]; then + L2_URL=https://goerli.optimism.io +fi + + +T0=`cast block-number` ; sleep 60 ; T1=`cast block-number` +PER_MIN=`expr $T1 - $T0` +echo Blocks per minute: $PER_MIN + + +if [ $PER_MIN -eq 0 ]; then + echo Not synching + exit; +fi + +# During that minute the head of the chain progressed by thirty blocks +PROGRESS_PER_MIN=`expr $PER_MIN - 30` +echo Progress per minute: $PROGRESS_PER_MIN + + +# How many more blocks do we need? +HEAD=`cast block-number --rpc-url $L2_URL` +BEHIND=`expr $HEAD - $T1` +MINUTES=`expr $BEHIND / $PROGRESS_PER_MIN` +HOURS=`expr $MINUTES / 60` +echo Hours until sync completed: $HOURS + +if [ $HOURS -gt 24 ] ; then + DAYS=`expr $HOURS / 24` + echo Days until sync complete: $DAYS +fi +``` + + +## Operations + +It is best to start `op-geth` first and shut it down last. diff --git a/src/docs/developers/node-oper/which-node.md b/src/docs/developers/node-oper/which-node.md new file mode 100644 index 000000000..8ded08538 --- /dev/null +++ b/src/docs/developers/node-oper/which-node.md @@ -0,0 +1,56 @@ +--- +title: Running your own node +lang: en-US +--- + +If you're looking to build an app on OP Mainnet you'll need access to an OP Mainnet node. You have two options - use a hosted node from providers like Alchemy or run your own. + + +## Hosted node providers + +You can get a free, hosted node from [any of these providers](../../useful-tools/providers.md) to get up and building quickly. +Of them, [Alchemy](https://www.alchemy.com/optimism) is our preferred node provider, and is used to power our [public endpoint](../../useful-tools/networks.md). + +Many applications include a server component that needs to communicate directly with the blockchain, if only to query for information. +Once an application's needs exceed the free tier, you can use one of the paid tiers to continue to use the hosted node provider. + + +## Should you run your own node? + +There are several advantages to running your own node: + +- **Cost**. If your application runs a lot of queries ([`eth_call`](https://docs.alchemy.com/reference/eth-call-optimism), [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas-optimism), etc.) it might be cheaper to run your own node instead of using a service provider. +- **Verification**. Some types of application need to *know* that the state commitments submitted by the sequencer are accurate. + For example, bridges can allow fast withdrawals because they know that the assets deposited with them on L2 really exist, and belong to the user depositing them. + Bridges achieve this, before the + + +Of course, there are some additional considerations and costs to running a node on your own: + +### Hardware + +Replicas need to store the transaction history of OP Mainnet (or the relevant OP testnet) and to run both `op-geth` and `op-node`. +They need to be relatively powerful machines (real or virtual). +We recommend at least 16 GB RAM, and an SSD drive with at least 500 GB free (for OP Mainnet). +Note that during installation time you'll need more storage because you temporarily need space for a compressed copy of the latest snapshot as well as the decompressed `op-geth` data directory. + + +### Synchronization + +[The `op-node` component](../bedrock/explainer.md#rollup-node) synchronizes from L1. +You need to get L1 Ethereum access from somewhere, either a node provider or a node that your organization runs. + +[The `op-geth` component](../bedrock/explainer.md#execution-client) synchronizes from both other OP Mainnet (or testnet) nodes (https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/specs/exec-engine.md#happy-path-sync), meaning L2, [and Ethereum (or the appropriate L1 testnet)](https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/specs/exec-engine.md#worst-case-sync) if necessary. + +To synchronize only from L1, you edit the [op-node configuration](https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/specs/rollup-node.md) to set `OP_NODE_P2P_DISABLE` to `true`. + + + + +### Upgrades + +At present, the best way to know when to upgrade your software is to look at the software releases on github. + + +- [`op-geth`](https://github.com/ethereum-optimism/op-geth/releases/latest) +- [`op-node`](https://github.com/ethereum-optimism/optimism/releases/), but because the monorepo is also used for other software, you might need to scroll down to see what is the latest `op-node` release. \ No newline at end of file diff --git a/src/x b/src/x new file mode 100644 index 000000000..bfba84a5f --- /dev/null +++ b/src/x @@ -0,0 +1,5 @@ + + - [OP Goerli](https://datadirs.optimism.io/goerli-bedrock.tar.zst) +This is the script for OP Goerli. +You can use this script, which uses [Foundry](https://book.getfoundry.sh/). and the UNIX Note that this script is for OP Goerli. +./docs/developers/node-oper/dockerless.md