Skip to content

Network & Hosts Toolkit

By Romi Fadli, S.Kom. 11 min read Environments & Deployment
On this page
A browser window sends one orange path into a single web server, which routes it on to the orange staging environment; dashed paths to the blue development and green production environments stay inactive

Network & Hosts Toolkit is a pair of commands that keep three environments apart on one machine and one web server. One sets an environment's addresses and brings every place that stores them into line in a single run; the other points the browser at the environment I choose. Neither asks me to remember where an address is written.

Development, staging, and production share one domain name and one nginx, and are separated only by the address each one listens on and the Docker network it runs in. That separation is only as good as its consistency: one stale address in one configuration file is enough to send a request to the wrong environment.

At a glance

Toolsset-network, switch-env
FormCommand-line scripts sharing one helper library
LanguageBash with awk for literal substitution, driving Docker and nginx
StatusStable, in production
Source codePrivate
What the toolkit is, what it is built with, and its state.

What it does

A. Sets an environment's addresses in one command

I give it an environment, a host address, and a subnet. The input is checked strictly — real IPv4 octets and a subnet in the expected form — and the gateway is derived from the subnet rather than asked for separately. The command then updates every place that holds an address: the recorded network state, the web-server configuration, the Docker network itself, which is recreated because a subnet cannot be changed in place, and the environment's folder, where the old gateway and the old range are replaced as text wherever they appear in some thirty-four configuration files. That replacement matches literally and writes the files' contents in place, so their owner, mode, and links survive a run with elevated rights.

B. Renders the web-server configuration from templates

The nginx configuration for each environment is generated from a template and installed as an ordinary root-owned file. Blocks that are identical in all three environments live in shared partials, so the only things a template carries of its own are what genuinely differs. Partials are inserted as text rather than with the web server's own include, because upstream names differ per environment, and the result is one flat file an operator can read; every upstream carries an environment prefix, because upstream names are global and a duplicate stops the web server. Each rendered file is stamped with the time and the template it came from, and installed with an atomic swap at mode 0644, which also replaces an old link — a temporary file is created readable by its owner only, and the web server's workers could not have read it. On a new machine, a placeholder for the staging version banner is created, so the first render does not wait for a staging environment that cannot exist yet. After a git pull, a render-only mode rewrites the configuration from the recorded state and touches nothing else, and it rejects extra arguments with an explanation, since they usually mean the full mode was intended.

C. Refuses before it touches anything

Before a single file changes, the command checks that the new gateway and host address are not already used by another environment, and that no other Docker network on the host, from any project, already owns the subnet. A clash stops the run with the name of the network in the way and the command that would remove it — the check exists because a subnet I chose once belonged to another project's network — and it is skipped only when the network is left alone on purpose. Before acting, the command prints what it will do, warns when the host address is not on any interface or when containers will be stopped, and asks me to type the environment's name, or accepts a flag for automation. Whether anything needs changing is judged from the installed web-server configuration as well, after a new machine once heard "no change" and was left without any configuration at all; and the output of the network inspection is cleaned before it is compared, after an empty answer made the command try to delete a network that never existed.

D. Points the browser at one environment

The second command rewrites one marked block in /etc/hosts so that both domains resolve to exactly one environment. It reads the addresses from the same recorded state, disables any other entry for those domains that would cause a double resolution, and leaves every other line alone. A none option hands both domains back to public DNS. The new file is validated before it is written — each domain active exactly once, at the requested address — and the run is abandoned if the file would shrink by more than twelve lines, the sign of unpaired markers eating it. After writing, the result is checked through the system resolver. When the target address is on no interface, the warning names the likely cause for that environment, such as a private network that is not connected or a laptop whose address changed, and only then asks for confirmation. The guide comments inside the block are generated from the real recorded addresses, after a fixed comment kept naming an address that was no longer used. Switching to the environment that is already active changes nothing, and the hosts file can be pointed at a test copy.

E. Shows the state of all three

Both commands have a show mode. The network one lists each environment's host address, gateway, and subnet, and flags a configuration whose host address is not present on any interface — treating the whole loopback range as present — a network that has not been created, and a subnet that differs from Docker's. The hosts one shows which environment the browser currently reaches.

How I use it

bash
set-network --show
set-network dev <host-address> <subnet>    # host address + subnet
set-network --render production                   # after git pull on a server
switch-env staging                                # browser now opens staging
switch-env --show
PlaceWhat one run changes
Recorded network statethe environment's gateway and listening address
Web-server configurationrendered again from the template, tested, then applied
Environment foldergateway, trusted-proxy ranges, host entries, service endpoints, and fallback values
Docker networkrecreated with the new subnet
Everything a single address change keeps in line.
Exit codeMeaning
0the environment uses its new addresses, or nothing needed changing
1a step failed, the number of arguments was wrong, or the recorded state lacked a key; an invalid web-server configuration was rolled back
2an invalid value, an unknown option, or an address clash; nothing was touched
How the network command reports its outcome.

Design decisions

One command owns every address, so they cannot drift apart. Gateway and subnet were once written by hand in different places and fell out of step; deriving one from the other makes that impossible. The gateway and the trusted-proxy ranges are rewritten in the same run, so every address derives from one source. And this command is the only place in the project where a Docker network is created, while the services merely join it, so the network and the web server's upstream addresses always come from the same run.

The configuration is rendered, never linked into the repository. The installed configuration used to be a symlink into the working tree, so an ordinary git checkout changed the web server that was serving traffic — and one did, sending staging and production to an old gateway and turning a live route into 502. Now git cannot reach nginx at all, and running the tool leaves the working tree clean. Which file holds a value matters too: on a machine with a local override file, the listening address I type is written there, so it always wins, after a typed address was once ignored and the configuration rendered with the old one. Templates use @@NAME@@ placeholders, because nginx configuration is full of its own $ variables and a plain environment substitution would silently erase $uri into a file that still passes nginx -t; a render that leaves a placeholder or an empty required directive fails instead. Shared blocks exist because one route once vanished from a single environment and was noticed only long afterwards.

It refuses early, and on a web server it does not own alone it rolls back what it can. A broken web-server configuration takes down every site it serves. Clashes are found before anything is written. The Docker network is created before the configuration is rendered, because the new gateway must exist on the host before nginx can listen on it. The old configuration is kept outside the directory nginx reads, since a backup file there would be parsed too; if the test fails, the configuration and the recorded state are restored and the server is tested again, and a rollback that still fails is reported as fatal with the old configuration kept. The test's result is captured in a way that strict error handling cannot cut short, after a test run showed the rollback branch was never reached and a broken configuration stayed while nginx died. The test also tells a broken configuration apart from one that cannot be tested for lack of rights, which keeps the file and the stash and prints the restore command instead of guessing, and a web server that is not running is started, with its failure counter reset when it has hit its start limit. It reloads when the set of listening addresses is unchanged, so other domains keep their connections, and restarts only when an address changes, because a reload never binds a new one. The show mode flags a missing host address precisely because nginx -t does not.

Switching environments is one everyday command. The hosts command reads addresses from the recorded state, so it follows the last network change without being told. It guarantees exactly one active address per domain and backs up the file before writing. It calls sudo itself, as the network command does through the shared library — a deliberate choice, because switching happens many times a day and the alternative is editing /etc/hosts by hand each time. After a switch it restarts the web server, so that addresses changed by the network command are actually bound.

It finishes even without a terminal. It is built to finish in non-interactive sessions too, after two such runs once stopped halfway, with the configuration written but the environment files and the network not.

Specifications

InputsAn environment, a host address, and a subnet; or an environment to render; or an environment to point the browser at
OutputsUpdated network state, a rendered web-server configuration, rewritten environment files, a recreated Docker network, and one active block in /etc/hosts
Per-machine valuesA local override file, kept out of version control, holds a machine's listening address; the gateway stays the same on every host
SafetyClash checks before writing; configuration tested before it is applied and rolled back if invalid; reload instead of restart when the listening addresses are unchanged; containers on a network are stopped before it is recreated; hosts file validated and backed up
RequirementsDocker and nginx on the host; elevated rights only for the web-server configuration and /etc/hosts
Inputs, outputs, and guarantees of the toolkit.

The story of serving three environments from one domain name that could not change.

Why I develop against a real domain and real HTTPS, which is what the hosts switch makes practical.

The release procedure, in which rendering the configuration again follows every pull on a server.

Each script follows the shape I give every small tool, in its Bash variant.

The toolkit that builds staging environments on the networks this one records.

Why each environment on one machine needs its own subnet, network, and identity.

The short commands I use to read which networks exist and which subnets each stack holds.

Terms

Subnet and gateway
The address range of a Docker network, and the host-side address through which its containers are reached.
Listening address
The address and port a web server accepts connections on; here, the line that separates one environment from another.
Reload and restart
Reload loads a new configuration while keeping open connections; restart stops and starts the server, which is the only way to bind a new address.
Hosts file
The local file, /etc/hosts, that maps names to addresses before DNS is asked.
Template rendering
Producing a configuration file by filling placeholders in a template with recorded values.
Trusted proxy
An address range a service believes when it reports the original client's address.

Technologies covered

  • Bash
  • Docker
  • nginx

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Four commands that move a stack from its old layout into a new structure across major versions: export without changing the source's data, import each store the way its version allows, verify what really arrived, and rename modules while moving their data along.

11 min read Read more →
A staging configuration with an orange banner and a production configuration with a green banner; ordinary keys cross over with arrows, while a locked secret is stopped halfway by a red cross and production keeps its own lock

Three commands that take a tested release into production without overwriting what production owns: they promote configuration structure but keep its secrets and addresses, refuse to copy new secrets, and apply schema migrations the images carry.

10 min read Read more →
A purple root circle linked to a blue and an orange intermediate circle, which lead to a signed document with a green seal and to a clock.

Aurograf

DevOps

The digital signature platform of the City Government of South Tangerang: signers prove who they are, sign PDFs to PAdES B-LTA with certificates from the platform's own authorities, and anyone can verify the result — version 2, which I designed, built and operate.

10 min read Read more →

Related Articles

A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

A single script that turns a project's Markdown documentation into a static site that opens from a folder with no server or internet: a menu, a table of contents that follows the reader, tables that never truncate, code with a copy button, and links that keep working.

7 min read Read more →
An old layout with a grey banner and a new structure with a green banner, each holding a blue database, a purple document store, and an orange telemetry store; three coloured paths pass through one export package marked with a green check

Four commands that move a stack from its old layout into a new structure across major versions: export without changing the source's data, import each store the way its version allows, verify what really arrived, and rename modules while moving their data along.

11 min read Read more →
Three image layer stacks, each with a version tag of its own colour, blue, orange, and green, each leading to an archive file banded in the same colour

Commands that version every module of a stack on its own, build its image under a tag that means exactly one build, refuse to reuse a version or swallow a failed compile, and archive images by name and tag so versions never overwrite each other.

7 min read Read more →