# Deploy microservices with Docker & autoscaling

Source: https://imqueue.org/tutorial/deployment/
Published: 2018-11-08
Updated: 2026-08-01
Author: @imqueue maintainers (https://github.com/imqueue)

Ship your services: per-service Docker images, environment-based configuration, and horizontal scaling for any load.

This chapter covers the various aspects of deploying @imqueue-based services.

There are many ways to deploy, depending on your needs. The most immediate is
development deployment; another is shipping production packages. The deployment
scheme also varies by environment — you might want to use every physical core of
a single server, or spread many small containerised images across a network.

@imqueue-based services are ready to meet any of these needs, but it's up to your
developers and DevOps to decide how. The @imqueue/cli default template also
provides some ready-to-use deployment functionality out of the box.

**For services to scale in any direction, they must either be stateless or
provide a mechanism to synchronise state across processes and network instances.
Keep this in mind throughout implementation.**

What does that mean in practice?

Imagine a service that keeps some state in memory — say, the list of
authenticated users:

~~~typescript
import { IMQService, expose } from '@imqueue/rpc';

class MyService extends IMQService {
    private usersList: any[] = [];

    @expose()
    public addUser(data: any) {
        this.usersList.push(data);
    }
}
~~~

Each time a remote client calls `addUser()`, the service mutates its in-memory
state. With a single instance running, this works fine. But launch several copies
and you're in trouble: after a few `addUser()` calls, each copy holds a different
internal state — and the divergence is unpredictable.

That's undesirable, so you must either implement a mechanism to share state
between copies, or store and mutate the state in an external tool such as a
database. Rolling your own state-sharing is usually non-trivial and prone to
side-effects, so unless you're confident in how it works, we recommend designing
your services to be stateless. Stateless services behave predictably across every
deployment scenario.

For example, this tutorial suggests implementing the
[Car service](https://github.com/imqueue-sandbox/car) with an in-memory car
database, which is stateful by design. But that database is largely static data:
we refresh it roughly once every 24 hours, all running copies do so at about the
same time, and we otherwise only read from it. There are minor side-effects, but
they're insignificant for this system. It's neither good nor bad — you just need
to understand what you're doing and what the consequences are.

## Scaling options

Services are designed to run in multi-process environments. Since JavaScript on
Node.js is single-threaded by nature, one process uses the power of only one
core. On a multi-core machine you'll usually want to use all available cores,
which you can do with a couple of configuration options. These are service
options (they live on `IMQServiceOptions`), and can be set in the service's
`config.ts`:

~~~typescript
export const serviceOptions: Partial<IMQServiceOptions> = {
    multiProcess: true,  // default: false — turned off
    childrenPerCore: 2,  // default: 1
};
~~~

Set `multiProcess: true` to enable multi-process mode (or manage the value
through environment variables). By default this forks one worker per available
core. In real-world runs you may find that using all available capacity requires
more than one process per core: increasing `childrenPerCore` adds more context
switching per core but can still yield an overall performance gain. Treat it as a
tuning knob — experiment to find the value that works best for your workload.

If your deployment is based on small single-core containers, you probably don't
need to touch the multi-process options at all. Either way, the right settings
come from testing and experimentation.

## Building containers

The @imqueue/cli default template gives each service a `Dockerfile` and a set of
Docker npm scripts, so you can build and run an image for any service locally:

~~~bash
npm run docker:build
npm run docker:run
npm run docker:stop
npm run docker:ssh
~~~

Local builds require, of course, a Docker engine installed on your machine.

Continuous integration runs on GitHub Actions. Every service created by
@imqueue/cli ships a `.github/workflows/build.yml` that, on each push and pull
request, installs dependencies and runs the test suite across the current LTS
and latest Node.js versions — verifying the build stays green. (Which CI
provider is wired in depends on how you configured the tool; GitHub Actions is
the default, with CircleCI and Travis also available.)

Building and publishing images to a container registry is left to your own
deployment pipeline — run `npm run docker:build` there and push the resulting
image. Pre-built Docker images can then be pulled and deployed across many cloud
environments — AWS, Azure, Google Cloud Platform and others. From there it's a
matter of configuring your cloud environment: enabling auto-scaling and anything
else you need.

One important note about running @imqueue clients in Docker containers: unless
you name your clients explicitly, each client generates a unique name based on
the operating system's UUID. Since Docker images share the same OS UUID out of
the box, you should set a unique value on the first image build — usually in
`/etc/machine-id` or `/var/lib/dbus/machine-id`. Consult the documentation for
your container's base OS to find the correct location.

## Environment variables

Environment variables are a powerful way to separate configuration across
environments without maintaining multiple config codebases. On cloud platforms
such as AWS you might use Parameter Store to supply configuration, while for local
development you can use `.env` files.

This requires some setup in the service's `config.ts`. Configure each option to
read from an environment variable first (which you define yourself) and fall back
to a default value. We covered this in
[chapter 2](https://imqueue.org/tutorial/user-service#configuring-the-service).

We strongly recommend following the same approach for any configuration in your
real-world services, and documenting the expected environment variables in your
README files — so that anyone deploying to a new environment can tune their setup
easily.

## Running it locally

We've covered the many options available when deploying @imqueue services. As you
can see, it's a flexible solution, able to satisfy any load and suitable for
horizontal scaling and cloud deployments.

For this tutorial we'll focus on the default development environment, so you can
run the example services from our
[codebase](https://github.com/imqueue-sandbox) and experiment with them.

First, clone all the repositories locally. Let's assume a dedicated directory,
for example `~/imqueue-sandbox`:

~~~bash
mkdir ~/imqueue-sandbox
cd ~/imqueue-sandbox
git clone git@github.com:imqueue-sandbox/api.git
git clone git@github.com:imqueue-sandbox/auth.git
git clone git@github.com:imqueue-sandbox/car.git
git clone git@github.com:imqueue-sandbox/time-table.git
git clone git@github.com:imqueue-sandbox/user.git
git clone git@github.com:imqueue-sandbox/web-app.git
~~~

Next, make sure Redis, MongoDB and PostgreSQL are running on your development
machine. Out of the box the services look for them at the standard local
addresses — `localhost:6379`, `localhost:27017` and `localhost:5432`.

You'll also need a PostgreSQL database named `tutmq`, owned by a user `tutmq`
with the password `tutmq` — or point the time-table service at a different
database through its `DB_CONN_STR` environment variable. The schema itself is
created on start-up, so there's nothing to migrate by hand. If you want to watch
the statements it issues, start it with `SQL_LOG=1`; `SQL_PRETTIFY=1` and
`SQL_COLORIZE=1` make them readable.

The auth service signs its JWTs with a secret you supply and refuses to start
without one. `.env` files are git-ignored, so a fresh clone has none — generate
your own secret before starting the fleet:

~~~bash
cd ~/imqueue-sandbox/auth
echo "JWT_KEY=$(openssl rand -hex 32)" > .env
~~~

Then install dependencies and run each service in its own terminal window (or
use a multiplexer such as `screen` or `tmux` if you prefer):

~~~bash
cd ~/imqueue-sandbox/[service_dir]
npm i
npm run dev
~~~

where `[service_dir]` is one of `user`, `auth`, `car`, `time-table` or `api`.
The services talk to each other over the message queue and ship pre-generated
static clients, so you can start them in any order — a service simply queues its
calls until the peer it needs becomes available.

Once the API service is running, the GraphiQL web interface is available at
[http://localhost:8888/](http://localhost:8888/).

Finally, install and start the React-based web interface:

~~~bash
cd ~/imqueue-sandbox/web-app
npm i
npm start
~~~

`npm start` first runs the Relay compiler over the TypeScript sources —
regenerating the typed query artifacts in the `__generated__` directories — and
then starts the Vite dev server. You can now use the application at
[http://localhost:3000/](http://localhost:3000/).

## Bonus: the same fleet, a different API

The tutorial proper ends here. But if you'd like proof that nothing about the
fleet ties you to GraphQL, carry on to the bonus chapters: we rebuild the API
gateway as a [REST/OpenAPI service](https://imqueue.org/tutorial/rest-api/) and add
[a second front-end](https://imqueue.org/tutorial/rest-web-app/) that speaks REST natively — both
stacks running side by side, without changing a single line in the back-end
services.

Next up: [Bonus: REST API](https://imqueue.org/tutorial/rest-api).

