Skip to content
Workflow DevOps

Release Workflow: Development to Production

By Romi Fadli, S.Kom. 9 min read Environments & Deployment
On this page
Three stages in a row — development, staging and production — with an image passing from development to staging, configuration promoted from staging to production, staging fed with real data restored from a production backup, and production marked done once its core function is proven

Release Workflow: Development to Production is the procedure every change of mine follows from the laptop where it is written to the production environment where people use it. It runs each time a new version of a service is ready, and it ends only when the production system has done its core job once with real data.

This page is not a deployment tutorial. It describes how I think about a job I repeat often: which steps I automated, which I deliberately left to a person, and why each step sits where it does.

At a glance

TriggerA new version of any service is ready to ship
FrequencyOnce per release, for one service or several
InputsThe changed code, a recent production backup, access to the staging environment
OutputProduction running the new version, with its configuration and database migrated
Done whenProduction has performed its core function once, end to end, with real data
When the workflow runs, what it needs, and how I know it is finished.

The recurring work

A release is not one action but a chain of them, and every link can fail in a way that only shows up later: an image that was tested but never published, a configuration file copied over production's secrets, a database migration that silently did not run, a system that reports itself healthy while its main feature is broken. Doing this by hand once is manageable. Doing it for every release of every service, months apart, is how steps get skipped.

So I turned the release into a procedure with a fixed order. Each step removes one risk before the next step could turn it into an outage, and the steps that are easy to get wrong are scripts rather than memories.

The flow

  1. Develop on my laptop against the real domain, check external services, run the tests.
  2. Build the image, then publish it as a separate step.
  3. Generate a staging environment from the latest production backup, and test the release there.
  4. Promote the configuration to production — a dry run first.
  5. Start production, migrate it, and check that it is healthy.
  6. Prove the core function once with real data, then free or remove staging.

Steps and the decisions behind them

  1. Develop against the real domain. My laptop serves the same domain over the same certificate as production, so cookies, redirects, and callbacks behave in development the way they will behave in production. I decided to reproduce production locally rather than approximate it, because the bugs that come from a different name only appear after release, in the worst place to debug them.

  2. Check external services before the code depends on them. When a release starts using a storage server, an identity provider, or an email account, I run a readiness check against its credentials first. A failure caught here is clearly the service's; the same failure caught after release looks like a bug in my code.

  3. Run the automated tests. The tests must pass before an image is built. An image that does not pass its own tests is not a candidate for release, so there is no reason to spend the rest of the procedure on it.

  4. Build the image, then publish it separately. Each service has its own version number, so a release normally rebuilds only the service that changed. The build script raises the version and builds the image, but it deliberately does not push it. Publishing is a separate, visible step, because a test of an image that was never published proves nothing about what the server will run.

    bash
    build <service>                   # raise the version, build the image — no push
    docker push <image>:<version>     # publishing is its own step
  5. Generate staging from the latest production backup. One command assembles the stack for this version, restores data from the backup, starts the services, applies database migrations, and runs a health check. The data comes from the backup, never from production directly, so staging can be rebuilt as often as I like without touching production. Using backups this way also proves, every release, that the backups can actually be restored.

  6. Test the release on staging. Staging holds real data and runs the published image, never a local one, so a test there exercises exactly what production will run against the same kind of records. This step stays manual: deciding that a release behaves correctly is a judgement, not a check a script can make.

  7. Promote the configuration — dry run first. Configuration is not copied from staging to production. The promotion script moves its structure: keys that were added or removed, and values that are not secret. Secrets and addresses stay production's own, a secret that exists only in staging stops the script until I set production's value by hand, and every file it writes is backed up first. The dry run shows me every change before anything is written.

    bash
    promote --from staging/<version> --dry-run   # show what would change
    promote --from staging/<version>             # then apply it
  8. Start production, migrate it, and check its health. The same migrations as on staging are applied explicitly, followed by the same health check. Migrations that a service does not run on start-up get their own step, so a schema change can never hide behind a container that started cleanly.

  9. Prove the core function once. Healthy containers do not prove that the system does its job. The release is not finished until production has performed its core function once, end to end, with real data — on a document-signing platform, for example, that means signing one real document.

  10. Free or remove staging. Once testing is over, staging is stopped to free memory on the server. When the version is no longer needed, it is removed entirely: containers, folder, state, and web server configuration, so old environments never pile up unnoticed.

What I automated, and what I did not

StepAutomated or manualWhy
Readiness checks, testsautomatedthe same questions every time; a machine asks them faster and never skips one
Buildautomatedraising a version and building an image must be identical every release
Publishmanual, deliberatelypushing an image is the point of no return for what the server runs; it should be a conscious act
Generate stagingautomated, one commandrestore, start, migrate, and check are too many steps to repeat by hand without missing one
Test on stagingmanualwhether a release behaves correctly is a judgement
Promote configurationautomated, with a dry runcopying files by hand is how secrets get overwritten; the dry run keeps me in the loop
New secretsmanuala secret value must be chosen for production, never inherited from staging
Migrate and health checkautomatedforgetting a migration fails silently
Prove the core functionmanualonly a real transaction proves the release; it is the definition of done
Remove stagingautomated, one commandcleaning up is easy to forget and tedious to do completely
Each step, whether it runs on its own, and the reason behind that choice.

When it fails

Before choosing a way back, I find out exactly where the release broke, and where I look depends on how it failed.

If production does not start or is not healthy, the cause is almost always printed where the failure happened: in the terminal output of the promotion and of starting the services. A missing variable, a migration that stopped, a container that exits — these show up there, not in the logs of a system that never came up.

If everything is up and healthy but a feature fails, such as a signature that does not complete, I trace the request through the logs and traces the services send to the OpenTelemetry Collector and on to Elasticsearch, to find the exact point where it went wrong.

Only then do I choose how to recover, depending on what the diagnosis shows:

  • Run the previous image when the new code is at fault and the data is intact.
  • Restore from the backup when a migration has already changed or damaged the data.
  • Fix forward when the problem is small and clear: correct it, take it through staging, and release a new version.

Why this order

The order moves every risk to the place where it is cheapest to meet. Problems with external services are caught on my laptop, before any code depends on them. Test data comes from a backup, so staging can be rebuilt freely and the backups are proven along the way. Configuration is promoted by structure, so a release can never overwrite production's secrets. Migrations are applied explicitly on both sides, so a schema change cannot hide behind a green health check. And the last step is a real transaction, because that is the one thing the system exists to do.

Variations

Only one service changed. This is the usual case. Only that service is built and published with a new version; every other service stays on the version it already runs, and staging is generated with that combination.

An urgent fix. A hotfix takes the same path, not a shortcut around it. What gets shorter is the test on staging, which covers only the part that was fixed instead of the whole release. Skipping staging entirely would save minutes and risk exactly the kind of failure the procedure exists to prevent.

Terms

Staging
An environment that mirrors production, with a copy of its data, used to test a release before it goes live.
Image
A packaged, versioned copy of a service and everything it needs to run, from which containers are started.
Migration
A versioned change to a database schema, applied in order and recorded so that it runs only once.
Dry run
Running a script so that it reports what it would change without changing anything.
Health check
An automatic test that a service has started and can answer, which says nothing about whether its features work.
Fix forward
Recovering from a failed release by releasing a corrected version, rather than going back to the previous one.

Technologies covered

  • Docker

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

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 →
Four identical grey keys on the left each become a key of its own colour on the right, every new key marked with a green check; below, a purple key stays behind a red bar

One command that rotates an environment's internal credentials key by key: a dry run by default, a backup first, a unique value for every key, a real login to prove each credential that can log in, and rollback for any that fail.

8 min read Read more →
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

Two commands that keep three environments apart on one machine and one web server: one sets an environment's addresses and aligns every file that stores them, rendering nginx from templates and refusing on clashes; the other points the browser at one environment.

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 →
Three sources on the left — configuration, container images, and a backup — flowing into one staging environment with an orange banner, its services, and a healthy check mark

Three commands that build a full copy of production for one release — configuration from the repository, images from the registry, data from the latest backup — restore it in order, run the release's migrations against that data, and remove it without a trace.

13 min read Read more →
A database, an object storage bucket, and a telemetry store on the left, each flowing into a stack of restore points on the right, the front one showing a timeline and a check mark

Three commands that back up a running Docker stack — PostgreSQL, MinIO, and Elasticsearch — without downtime, restore it to any point, and keep retention safe, with exit codes that say how much a failure matters.

12 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 →
A terminal window listing passing checks, connected to a blue storage bucket with a padlock and a green storage bucket without one.

A single Python script that proves S3-compatible storage is ready before an application depends on it, by using every bucket the way a real client does and catching the failures that return HTTP 200.

12 min read Read more →

Related Articles

Three sources on the left — configuration, container images, and a backup — flowing into one staging environment with an orange banner, its services, and a healthy check mark

Three commands that build a full copy of production for one release — configuration from the repository, images from the registry, data from the latest backup — restore it in order, run the release's migrations against that data, and remove it without a trace.

13 min read Read more →
A database, an object storage bucket, and a telemetry store on the left, each flowing into a stack of restore points on the right, the front one showing a timeline and a check mark

Three commands that back up a running Docker stack — PostgreSQL, MinIO, and Elasticsearch — without downtime, restore it to any point, and keep retention safe, with exit codes that say how much a failure matters.

12 min read Read more →
Two folder panels side by side; the destination holds one extra file marked with a cross

cp -r drops owner, permissions and timestamps, producing copies that look right and fail in use. A 58-line rsync wrapper makes the destination truly equal to the source — and most of its lines are refusals, because making it equal means agreeing to delete.

21 min read Read more →