Motivation#
The previous post covered system state and backups. Deploying services brings these topics together: where software comes from, how it starts, where data is written and which computers can access it.
The earlier Docker usage post introduced organizing containers with Compose. NixOS can also manage services through system modules. Here we will compare the approaches and explain startup relationships, data directories and networking.
The examples use a fictional laptop host, the reserved example domain demo.invalid and demonstration pages. They contain no existing service configuration. We begin on the local loopback address, with no real domain or public port required.
Service Deployment Methods#
Three common approaches are:
| Method | Configuration | Suitable use |
|---|---|---|
| NixOS service module | System options such as services.* | A suitable module exists and you want integrated accounts, configuration and startup |
| Custom systemd service | systemd.services.* | Your program or script needs an explicit identity and startup conditions |
| Docker or Podman container | Compose or NixOS container options | An application provides an image, or an existing container deployment is worth retaining |
Installing a package does not necessarily enable its service. A module may also generate configuration, accounts and systemd units. Its settings can differ from upstream defaults, so inspect the options available in your locked version.
Containers still need startup, updates, secrets, data and networking managed. You can use the NixOS OCI container module or retain Compose files. Decide which tool starts each group to avoid configurations competing for ports or data directories.
How the Services Are Organized Now#
Container services on these computers remain defined and started through Compose. NixOS manages the container engine, startup preparation of runtime configuration and host services. Shared Compose definitions are separate from host differences, and application source may be a separate Git submodule. Moving a service changes its target-host arrangement without requiring the whole Compose deployment to be rewritten as Nix expressions.
Deployment checks encrypted inputs and ordinary parameters, produces restricted runtime files, selects the relevant Compose combination, then rebuilds or recreates the affected service. A NixOS rebuild and a Compose deployment are distinct operations. Changing an image or host parameter may not require rebuilding the whole system, but runtime configuration still needs preparing and the service needs verification.
Access primarily uses Traefik with tunnel or direct routes. Proxy-facing containers join a shared proxy network. Applications reachable through that network need no additional host port publication. Routes and host differences are recorded separately, and public and direct entry points are verified independently. An unpublished host port does not mean a proxy-provided entry point is unexposed.
A migration also needs persistent data, decryption access, image availability and the original instance stopped. Startup is only one part; health and access routes need checking at the destination.
The Nginx and small-container examples below teach service modules, listening addresses and proxy relationships. They are not the current edge proxy configuration. The DynamicUser exercise explains directory management rather than claiming that all existing services use dynamic accounts.
Using a NixOS Service Module#
Configuring a Demonstration Nginx Page#
Create modules/service-demo.nix in the existing configuration and import it in the host’s module list:
| |
This enables Nginx and binds the demonstration virtual host to 127.0.0.1:8088. writeTextDir produces a static page in the store, suitable for content with no secrets or runtime writes. Real uploads and databases need separate data directories.
The exercise assumes an available port and a configuration where Nginx can be adjusted. With an existing deployment, preserve its virtual hosts and listeners and avoid name conflicts. Making one demonstration host listen on loopback does not restrict the other virtual hosts.
See the NixOS Nginx module for its options.
Building and Checking#
| |
Replace laptop with your output name. After building, review and apply using the process in post five, then inspect the service and page:
| |
Expect a loopback listener and HTML containing NixOS service demo. The Host header selects the example virtual host without configuring DNS for demo.invalid.
An active service means systemd considers it in the relevant running state. The listener and response verify separate things. A real service also needs functional tests such as login and data access.
systemd Startup Relationships#
Services often depend on mounts, databases or runtime secrets. Systemd handles pulling in another unit separately from ordering it:
| Setting | Purpose |
|---|---|
wantedBy | Include the service in a target’s startup arrangement |
wants | Pull in listed units; their failure normally does not directly block this service |
requires | Establish a stronger dependency; failure behavior also depends on ordering and other settings |
after or before | Order units being started together; these do not pull units in by themselves |
A database client may need both the database unit started and ordering after it. A started database process may still be unable to accept connections. The client needs retries, timeouts or appropriate readiness checks.
network-online.target represents completion of the configured network wait mechanism. It does not guarantee continued access to a particular remote site or database. Add it where needed; a local static page normally has no such dependency.
See the systemd unit documentation. Compose has a similar distinction: ordinary depends_on mainly arranges startup, while service_healthy waits for the declared health check. Dependencies can fail again after a check passes. See the Compose dependency documentation.
Service Identities and Data Directories#
For your own script, declare its identity and directories through systemd. This is a separate demonstration module:
| |
StateDirectory prepares persistent storage and exposes its path through STATE_DIRECTORY. DynamicUser assigns a temporary runtime identity and works with systemd-managed directories. Avoid using its potentially changing UID to manually arrange ownership elsewhere.
Run and inspect it with:
| |
It is a oneshot task, so inactive after successful exit is normal. This file contains only demonstration text. Real state files may hold private content and should not be printed into logs or chats in the same way.
With DynamicUser, persistent storage may be protected through paths such as /var/lib/private; confirm the actual path and permissions when backing it up. RuntimeDirectory holds temporary runtime files tied to the service lifecycle and is unsuitable for persistent storage. See the systemd directory and identity documentation.
System rollback does not restore these files automatically. Container volumes need separate recovery too. See post nine for runtime secrets.
Deploying a Container Service#
For the container alternative, create compose.yaml in a new demonstration directory:
| |
This uses the same host port as the native example. Remove or adjust that example’s 8088 listener before starting the container. Docker and Compose must already be available; this file does not configure the engine.
The image tag illustrates the configuration format. It is not a reason to keep that release indefinitely. For real deployments, choose a supported version, record the image digest and arrange updates.
From the demonstration directory, run:
| |
Expect Container service demo in the response. The host’s site directory is mounted read-only. Applications writing databases or uploads need their write locations and backups specified separately.
The explicit 127.0.0.1 publishes only on the host’s loopback address. Omitting the address normally publishes on all host addresses, so it is unsuitable as an implicit private-access policy. Docker versions and network modes can affect reachability; the official documentation also records a same-network issue with loopback publishing in older versions. See the Docker port publishing documentation.
To stop the demonstration containers from this directory:
| |
The host’s site directory remains. Avoid casually adding -v to delete data volumes in real deployments. Stopping a container does not back up its data.
Network Access and Reverse Proxies#
Consider listening addresses, host firewall rules, routing and application permissions separately. Opening a firewall port does not make a loopback-only process directly accept LAN connections. Listening on all addresses does not guarantee every network can reach it either.
Docker manages forwarding and firewall rules. Published container traffic may follow a different path from ordinary host-service traffic. The NixOS allowedTCPPorts list alone cannot establish that a container port is unexposed. See the Docker firewall documentation and test from the actual access location.
A reverse proxy provides an entry point forwarding requests to a backend. For example, host Nginx can proxy the container above with:
| |
This fragment requires Nginx enabled. Replace the previous virtual host of the same name rather than merging duplicate definitions. After applying, check:
| |
The entry point is now 8089, with the backend still on 8088. If Nginx itself is containerized, 127.0.0.1 normally refers to its own network environment. Use the appropriate service name or host address for that arrangement.
A real public entry point also needs a domain, TLS, authentication and access policy. A reverse proxy does not automatically add login protection. This example demonstrates forwarding without configuring a public site.
Troubleshooting#
I recommend checking in this order:
- Confirm the module is imported by the intended host and the configuration was built and applied.
- Check service or container startup and the first relevant log error.
- Inspect listening addresses, ports and conflicts.
- Test the backend, local entry point, LAN and external access locations separately.
- Check DNS, TLS, authentication and actual application behavior.
If the backend works locally but the proxy fails, inspect its target and network environment. If the proxy works but another computer cannot connect, inspect the entry point, firewall and routing. Change one part at a time instead of disabling several access controls to make a page load.
Recommended Usage#
I recommend checking for a suitable NixOS module first. Systemd can give your own programs explicit identities and data directories. Applications with established container deployments can retain Compose, with consistent configuration locations and a recorded startup method.
When asking AI for help, have it explain the service account, persistent data, secrets, listening addresses and startup conditions. Verify processes, access scope and function separately after changes, and prepare data recovery as described in the previous post.
The next post will cover updates and deployment across computers, including input updates, build resources and remote verification.
