Command Line, Processes, and Environment Variables
LESSON
Command Line, Processes, and Environment Variables
By the end of this lesson, you will be able to...
Trace how a backend service starts from a command into a running process.
Separate command-line arguments, environment variables, files, ports, streams, and exit status.
Diagnose startup failures such as missing config, wrong ports, bad working directories, and misleading exit codes.
Idea in one sentence: A backend service is not just code on disk; it is a process started with an explicit contract, and many deployment bugs come from that contract being hidden or wrong.
Core Insight
In the previous lesson, a proxy returned:
502 Bad Gateway
error=connection_refused
upstream=auth-service:8080
The request reached the proxy, but it probably did not reach the backend handler. The next question is not "which route is broken?" The next question is smaller:
Is there a process listening where the proxy expects one?
That question moves us from request paths to process startup.
Imagine the same orders API. On your laptop it works:
npm run dev
On a teammate's laptop it exits with:
DATABASE_URL is required
In staging, the container starts, but the proxy still returns 502. The platform expected the service to listen on port 8080; the service printed:
Listening on 3000
The naive idea is:
Run the app.
That phrase hides the hard part. Before an API can handle a request, the operating system must start a process. That process receives inputs. It reads configuration. It opens files. It binds a port. It reports success or failure. If any of those startup steps are wrong, the handler may never run.
The useful model is:
parent starts process -> process receives startup inputs -> app validates contract -> app binds port or exits
This lesson makes that startup mechanism visible.
The Moving Parts
Use one small service through the lesson:
PORT=8080 DATABASE_URL=postgres://db/orders ./orders-api --mode web
That line looks like one command. It is really several inputs crossing one boundary.
The parent is the thing that starts the service. Locally, the parent may be your shell. In production, it may be Docker, Kubernetes, systemd, a process manager, a test runner, or a platform launcher.
The child is the backend process.
The child receives:
- an executable path, such as
./orders-api - command-line arguments, such as
--mode web - environment variables, such as
PORT=8080 - a working directory, used for relative paths
- permissions and user identity
- standard input, standard output, and standard error
- signals, such as a request to stop
- a parent that can observe exit status
Plain meaning:
A startup contract is the list of things the process needs from its parent before it can do its job.
In this scenario:
The orders API needs a valid PORT, a valid DATABASE_URL, a mode, access to any required files, and a clear way to report whether startup succeeded.
Technical name:
We will call this the process startup contract.
This is a boundary, just like the boundaries in the request path. The proxy cannot forward to a service that did not start, did not bind the expected port, or exited while the platform thought it was healthy.
The Mechanism Step By Step
Follow the command from text to listening service.
Input:
PORT=8080 DATABASE_URL=postgres://db/orders ./orders-api --mode web
Step 1: parent prepares the launch
parent decides executable, arguments, environment, working directory, streams
Step 2: operating system creates a process
process gets a process id, memory, file descriptors, permissions
Step 3: program starts running
runtime exposes args, env vars, working directory, stdout, stderr
Step 4: app reads startup inputs
mode=web
PORT=8080
DATABASE_URL=postgres://db/orders
Step 5: app validates required inputs
port must be an integer from 1 to 65535
database URL must exist and parse as a database connection string
Step 6: app opens required resources
create logger
connect or prepare connection pool
load required files if any
Step 7: app binds the network port
listen on 0.0.0.0:8080 or another configured interface
Output:
success: process stays alive and reports ready
failure: process writes a clear error and exits nonzero
The important detail is that startup has intermediate states. A service can exist as a process but still not be ready. It may be running, but not listening. It may be listening on the wrong port. It may be listening, but using fallback configuration that points to the wrong database.
That is why "the container is running" is not the same as "the backend is ready."
So far, the mechanism is:
start process -> validate contract -> bind expected port -> report readiness
Arguments, Environment, And Files
Command-line arguments, environment variables, and files can all configure a backend. They are not the same tool.
Command-line arguments are good for visible choices for this run:
./orders-api --mode web --workers 4
They are easy to see in the command. They are useful for choices like mode, one-time maintenance actions, or worker count. They are a poor fit for secrets because command text may appear in shell history, process listings, logs, or error reports.
Environment variables are good for deploy-time configuration:
PORT=8080
DATABASE_URL=postgres://db/orders
LOG_LEVEL=info
They let the same artifact run in different places. The code can be the same in development, staging, and production while the parent supplies different values. The cost is that environment variables are strings. PORT=abc is still an environment variable. It becomes a useful setting only after the app parses and validates it.
Files are good for structured data that is deliberately packaged or mounted:
config/routes.json
schema.sql
certificates/ca.pem
Files bring their own questions. Is the file present in the container image? Is it mounted by the platform? Is the working directory what the app expects? Can the process read it with its permissions?
The design decision is not "arguments versus environment versus files forever." The decision is:
Which parent supplies this value?
When should the app reject it?
How will a human see what happened?
A Worked Startup Trace
Now trace a failure that explains the 502 from the previous lesson.
The deploy platform starts the process like this:
PORT=8080 DATABASE_URL=postgres://db/orders ./orders-api --mode web
The proxy is configured to forward to:
orders-service:8080
The application code accidentally ignores PORT and uses a hard-coded default:
listen_port = 3000
The trace looks like this:
Input:
parent supplies PORT=8080
proxy expects orders-service:8080
Transition:
process starts successfully
app reads DATABASE_URL
app ignores PORT
app binds 0.0.0.0:3000
Intermediate state:
process is running
port 3000 is listening
port 8080 is not listening
Output:
app prints "Listening on 3000"
platform sees a process
proxy connection to 8080 is refused
user sees 502 Bad Gateway
Naive failure contrast:
"The app is running, so the proxy must be wrong."
Better diagnosis:
The process exists, but it violated the startup contract.
It did not listen on the port supplied by its parent.
This is the small trap. A process can be alive and still unusable by the rest of the system. The startup contract connects the process boundary to the request boundary.
Check: If the proxy says connection_refused for orders-service:8080, and the app log says Listening on 3000, should you change the route handler first?
Think first, then reveal.
Answer: No. The request has not reached the handler. The first fix is in the startup contract: the app must read and validate the parent-supplied port, or the proxy must be configured to the port the app actually binds.
Fail Fast, Then Report Clearly
Some startup failures should stop the process immediately.
If DATABASE_URL is required for normal requests, this is a bad startup behavior:
DATABASE_URL missing
using development fallback database
Listening on 8080
exit status: still running
The process is alive, but it is now dangerous. It may accept real traffic while pointing at the wrong database.
A better behavior is:
stderr:
startup error: DATABASE_URL is required
exit status:
nonzero
This gives the parent a clear signal. A deploy platform, process manager, or test runner can treat nonzero exit status as startup failure. A human can read stderr and see the missing input.
Use this startup rule:
If the service cannot keep its basic promise, fail before accepting traffic.
That rule is not about being dramatic. It is about avoiding ambiguous failures later. A missing database URL discovered at startup is one clear problem. A missing database URL discovered after users send traffic becomes many symptoms: 500s, retries, noisy logs, and maybe corrupted assumptions.
Check: A service prints missing PORT, then exits with status 0. What is wrong with that signal?
Think first, then reveal.
Answer: Exit status 0 usually means success. If startup failed, the process should exit nonzero so automation can detect the failure. Text alone is not enough when another program is supervising the service.
Common Confusions
Confusion: "Environment variables are configuration, so they must be valid"
Why it is tempting:
The parent supplied a value, so it feels official.
Better model:
Environment variables are untrusted strings until the app parses and validates them. PORT=8080 may be valid. PORT=eight is just text. The app owns the validation boundary.
Confusion: "A running process means the service is ready"
Why it is tempting:
Process lists, container dashboards, and local terminals often show whether something is running.
Better model:
Running only means the operating system has a live process. Ready means the process has satisfied the startup contract and can accept the kind of work its parent or proxy will send. A service can be running while listening on the wrong port, missing a required file, or failing dependency setup.
Confusion: "Local .env files are the contract"
Why it is tempting:
Local .env files are convenient. They make the app start quickly while developing.
Better model:
A .env file can help local development, but the contract is the named set of required inputs and validations. If production does not have the same file, or if the file is untracked and undocumented, the real contract is hidden in one machine.
Trade-offs And Limits
The main trade-off is portable startup versus local convenience.
Explicit startup contracts make services easier to move from laptop to test runner to container to production. They also require discipline. You need to name required inputs, validate them, choose good defaults carefully, document how the parent supplies them, and avoid logging secrets while explaining failures.
This helps when:
the same service must start in more than one environment
automation must decide whether startup succeeded
humans need clear failure messages
It costs:
more validation code
more startup tests
less tolerance for "it works on my shell"
It does not solve every configuration problem. Environment variables are not a full secrets-management system. They do not handle rotation, access control, audit trails, or secret leakage by themselves. Later lessons will return to configuration and secrets with more care.
The signal that you are near the boundary is simple: people start saying "but it works on my machine." That often means the process contract is living in one machine instead of in the service's explicit startup behavior.
Practice
Design a startup contract for this service:
image-resizer --workers 4
The service reads jobs from a queue, writes resized images to object storage, and exposes a health endpoint for the platform.
Write a small contract with:
- two required environment variables
- one command-line argument
- one file or permission assumption
- one startup validation rule
- one readiness signal
- one nonzero-exit rule
A good answer should mention:
QUEUE_URLas a required environment variable, because the queue location changes by deployment.OBJECT_STORE_BUCKETorOBJECT_STORE_URLas a required environment variable, because output storage is external.--workers 4as a command-line argument, because worker count is a visible run choice.- A validation rule such as "workers must be a positive integer" and "required environment variables must exist before work starts."
- A readiness signal such as "listen on
$PORTand return healthy only after configuration validates." - A nonzero exit if required configuration is missing or invalid, with a clear message to
stderrthat does not print secrets.
Resources
- [DOC] GNU Bash Manual: Shell Commands
- Link: https://www.gnu.org/software/bash/manual/html_node/Shell-Commands.html
- Focus: Read for how a shell turns text into commands, arguments, and process execution.
- [DOC] Python documentation: Command line and environment
- Link: https://docs.python.org/3/using/cmdline.html
- Focus: Notice how one runtime exposes command-line options and environment variables.
- [DOC] Node.js documentation: process.env
- Link: https://nodejs.org/api/process.html#processenv
- Focus: Use this as a concrete example of how backend code reads environment variables as strings.
- [ARTICLE] The Twelve-Factor App: Config
- Link: https://12factor.net/config
- Focus: Read the argument for separating deploy-time configuration from source code.
Key Takeaways
- A backend service starts as an operating-system process before it becomes a request handler.
- A process startup contract includes executable, arguments, environment, working directory, files, streams, ports, and exit status.
- A running process is not automatically a ready service; it must bind the expected port and satisfy required inputs.
- Startup validation turns hidden machine state into a clear success or failure signal.
- Good startup failures use
stderrand nonzero exit status without leaking secrets.
← Back to Backend Development Foundations