How to deploy a Meteor project on your VPS using Docker
19 Jan 2016

I have a lot of little Meteor projects. Here’s the exact steps that I take to deploy them on a VPS in a resource-efficient manner. A cheap VPS with 1GB of RAM costs about $10/month and can support dozens of small Meteor deployments on its own.

I use a shared MongoDB instance and run the Meteor projects in Docker containers. nginx sits at the front end (port 80) and directs traffic to the appropriate Meteor project.

For the whole server

I’m starting with an Ubuntu 14.04 LTS 64-bit server running on DigitalOcean. The same should work for any recent Debian-like distribution and any VPS host (e.g. Amazon EC2). I assume that you are running as root.

Install packages

aptitude update
aptitude upgrade -y
aptitude install nginx mongodb 

Follow the instructions to install Docker. If you don’t want to read all of that, paste the following into your terminal:

apt-key adv --keyserver hkp:// --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
echo "deb ubuntu-trusty main" | sudo tee /etc/apt/sources.list.d/docker.list
aptitude update
aptitude install docker-engine

Download the relevant Docker images

docker pull meteorhacks/meteord:base

Expose MongoDB to Docker containers

In /etc/mongodb.conf, change:

bind_ip =


bind_ip =,

For each Meteor application you want to deploy

Add a database and user to Mongo

Run mongo and enter the following.

use <databasename>
db.addUser( { user: "<username>",
              pwd: "<password>",
              roles: [ "readWrite", "dbAdmin" ]
            } )

Replace <databasename>, <username> and <password> as appropriate.

Note that this uses Mongo polling, not the oplog.

But… the oplog! Doesn’t polling suck?

Yes, polling sucks. Remember that this setup is meant for small deployments and multiple deployments where you have many applications sharing the same MongoDB. I haven’t figured out (yet!) how to securely share the oplog between applications – you don’t want one insecure application to compromise the others. If this installation started to grow, you might notice that your machine load was high (maybe due to polling, maybe something else) and you’d be best moving to a dedicated MongoDB server with oplog access. But you’re small for now, so don’t sweat it. Premature optimisation is the root of all evil, etc.

Set up your nginx frontend proxy

In /etc/nginx/sites-enabled/<sitename>:

server {
        server_name <hostname>;

        access_log on;

        location / {
                proxy_pass         http://localhost:9001;
                proxy_redirect     off;

                proxy_set_header   Host              $host;
                proxy_set_header   X-Real-IP         $remote_addr;
                proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
                proxy_set_header   X-Forwarded-Proto $scheme;

Replace <sitename> with a shortname for your project (e.g. todolist) and <hostname> with the URL that you want to access your project at (e.g.

Every time you update a new version of the application (and the first time)

Build a Meteor bundle

Within the Meteor project directory:

meteor build --architecture=os.linux.x86_64 ./

This will create a new .tar.gz file in your project directory.

(The minification process is a little stricter than the standard meteor run, so you might run into new syntax errors and the like.)

Copy it to the Docker host

You need a directory to store the bundles; all of the .tar.gz files in the directory will be decompressed by the Docker image.

rsync --inplace -vP <bundlename>.tar.gz [email protected]:/opt/whatever/whatever.tar.gz

Each bundle either needs to go into a unique directory, or you need to clear out old ones when you upload new ones.

Shut down the old container (if necessary)

Run the new container

docker run -d \
    -e ROOT_URL=http://<app-url> \
    -e MONGO_URL=mongodb://<user>:<password>@<database-name> \
    -v /<bundle-dir>:/bundle \
    -p 9001:80 \

Note that is the default IP address of the host where we are running the MongoDB server.

comments powered by Disqus