diff --git a/deployment/local-testing/README.md b/deployment/local-testing/README.md
deleted file mode 100644
index 6e68e2ea18..0000000000
--- a/deployment/local-testing/README.md
+++ /dev/null
@@ -1,20 +0,0 @@
-# Local test deployment
-
-Currently, we're deploying to a vServer. The following will set up virtual machines locally in order to test out the deployment before it hits `staging` or `production`.
-
-1. Install [Vagrant](https://www.vagrantup.com/) on your machine
-
-1. `cd deployment/local-testing`
-
-1. `vagrant up` .. and wait
-
-1. Following services are running:
- -
- -
- -
- -
- -
-
-1. Be amazed
-
-In the future we might want to use [Ansible](https://www.ansible.com/) for provisioning. We could run our playbooks against this virtual machine, too.
diff --git a/deployment/local-testing/bootstrap.sh b/deployment/local-testing/bootstrap.sh
deleted file mode 100644
index 680640e91b..0000000000
--- a/deployment/local-testing/bootstrap.sh
+++ /dev/null
@@ -1,40 +0,0 @@
-#!/bin/sh
-
-apk update
-apk upgrade
-apk add nginx openrc nodejs npm git mysql mysql-client
-
-
-npm install pm2 -g
-pm2 startup
-
-rc-update add pm2 boot
-rc-update add nginx boot
-service nginx start
-
-
-service mariadb setup
-rc-update add mariadb boot
-sed -e '/skip-networking/ s/^#*/#/' -i /etc/my.cnf.d/mariadb-server.cnf
-service mariadb start
-
-
-cd /var/www/localhost/htdocs/
-git clone https://github.com/dreammall-earth/dreammall.earth.git
-cd dreammall.earth
-
-mkdir -p /etc/nginx/http.d
-cp -f ./deployment/nginx/default.conf /etc/nginx/http.d/default.conf
-cp ./deployment/nginx/frontend.conf /etc/nginx/http.d/frontend.conf
-cp ./deployment/nginx/admin.conf /etc/nginx/http.d/admin.conf
-
-service nginx restart
-
-mysql -e "CREATE USER 'dreammall'@'localhost' IDENTIFIED BY 'SECRET'; GRANT ALL PRIVILEGES ON * . * TO 'dreammall'@'localhost'; FLUSH PRIVILEGES;"
-
-cp backend/.env.dist backend/.env
-cp presenter/.env.dist presenter/.env
-cp frontend/.env.dist frontend/.env
-cp admin/.env.dist admin/.env
-
-deployment/deploy.sh
diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore
new file mode 100644
index 0000000000..fe52925b08
--- /dev/null
+++ b/infrastructure/.gitignore
@@ -0,0 +1,3 @@
+packer-output/
+archive/**/*
+!archive/.gitkeep
diff --git a/infrastructure/README.md b/infrastructure/README.md
new file mode 100644
index 0000000000..f38443fcc6
--- /dev/null
+++ b/infrastructure/README.md
@@ -0,0 +1,31 @@
+# Infrastructure
+
+This folder contains our infrastructure as code.
+
+The [benefits](https://www.redhat.com/en/topics/automation/what-is-infrastructure-as-code-iac#benefits-of-iac) of this paradigm are:
+- Cost reduction
+- Increase in speed of deployments
+- Reduce errors
+- Improve infrastructure consistency
+- Eliminate configuration drift
+
+## Local setup
+
+The following will set up virtual machines locally so you can test out if
+a change breaks the deployment.
+
+1. Install [packer](https://www.packer.io/) on your machine
+1. `cd infrastructure/`
+1. `packer init ./vagrant.pkr.hcl`
+
+### (Re)build Vagrant box
+
+1. Install [Vagrant](https://www.vagrantup.com/) and [VirtualBox](https://www.virtualbox.org/) on your machine
+1. `packer build --force --on-error=ask ./vagrant.pkr.hcl`
+1. `cd vagrant && vagrant up`
+1. Following services are running:
+-
+-
+-
+-
+-
diff --git a/infrastructure/archive/.gitkeep b/infrastructure/archive/.gitkeep
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/infrastructure/bootstrap.sh b/infrastructure/bootstrap.sh
new file mode 100644
index 0000000000..0db47a6b70
--- /dev/null
+++ b/infrastructure/bootstrap.sh
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+
+set -euxo pipefail
+
+apk update
+apk upgrade
+apk add nginx openrc nodejs npm git mysql mysql-client
+
+npm install -g pnpm pm2
+pm2 startup
+
+rc-update add pm2 boot
+rc-update add nginx boot
+service nginx start
+
+service mariadb setup
+rc-update add mariadb boot
+sed -e '/skip-networking/ s/^#*/#/' -i /etc/my.cnf.d/mariadb-server.cnf
+service mariadb start
+
+
+PROJECT_ROOT=/var/www/localhost/htdocs/dreammall.earth
+
+mkdir -p /etc/nginx/http.d
+cp -f $PROJECT_ROOT/deployment/nginx/default.conf /etc/nginx/http.d/default.conf
+cp $PROJECT_ROOT/deployment/nginx/frontend.conf /etc/nginx/http.d/frontend.conf
+cp $PROJECT_ROOT/deployment/nginx/admin.conf /etc/nginx/http.d/admin.conf
+
+mysql -e "CREATE USER 'dreammall'@'localhost' IDENTIFIED BY 'SECRET'; GRANT ALL PRIVILEGES ON * . * TO 'dreammall'@'localhost'; FLUSH PRIVILEGES;"
+
+cp $PROJECT_ROOT/backend/.env.dist $PROJECT_ROOT/backend/.env
+cp $PROJECT_ROOT/presenter/.env.dist $PROJECT_ROOT/presenter/.env
+cp $PROJECT_ROOT/frontend/.env.dist $PROJECT_ROOT/frontend/.env
+cp $PROJECT_ROOT/admin/.env.dist $PROJECT_ROOT/admin/.env
+
+$PROJECT_ROOT/deployment/deploy.sh
diff --git a/infrastructure/vagrant.pkr.hcl b/infrastructure/vagrant.pkr.hcl
new file mode 100644
index 0000000000..2824e7ba97
--- /dev/null
+++ b/infrastructure/vagrant.pkr.hcl
@@ -0,0 +1,69 @@
+packer {
+ required_plugins {
+ vagrant = {
+ version = "~> 1"
+ source = "github.com/hashicorp/vagrant"
+ }
+ }
+}
+
+source "vagrant" "dreammall" {
+ communicator = "ssh"
+ source_path = "generic/alpine319"
+ provider = "virtualbox"
+ add_force = true
+
+ output_dir = "packer-output"
+}
+
+build {
+ sources = ["sources.vagrant.dreammall"]
+
+
+ provisioner "shell-local" {
+ inline = [
+ "rm -rf archive",
+ "git clone ../ archive",
+ "cd archive",
+ "git remote set-url origin https://github.com/dreammall-earth/dreammall.earth.git", # only necessary because of the `git pull -ff` in `deploy.sh`
+ "git checkout master",
+ "git pull -ff",
+ "touch .gitkeep",
+ ]
+ }
+
+ provisioner "file" {
+ source = "archive"
+ destination = "/tmp"
+ }
+
+ provisioner "shell" {
+ execute_command = "echo 'packer' | sudo -S sh -c '{{ .Vars }} {{ .Path }}'"
+ inline = [
+ "mkdir -p /var/www/localhost/htdocs/",
+ "mv /tmp/archive /var/www/localhost/htdocs/dreammall.earth",
+ ]
+ }
+
+ provisioner "shell" {
+ execute_command = "echo 'packer' | sudo -S sh -c '{{ .Vars }} {{ .Path }}'"
+ script = "./bootstrap.sh"
+ }
+
+ provisioner "shell-local" {
+ inline = [
+ "rm -rf archive/",
+ "mkdir -p archive/",
+ "touch archive/.gitkeep",
+ ]
+ }
+
+ error-cleanup-provisioner "shell-local" {
+ inline = [
+ "rm -rf archive/",
+ "mkdir -p archive/",
+ "touch archive/.gitkeep",
+ ]
+ }
+}
+
diff --git a/deployment/local-testing/.gitignore b/infrastructure/vagrant/.gitignore
similarity index 100%
rename from deployment/local-testing/.gitignore
rename to infrastructure/vagrant/.gitignore
diff --git a/deployment/local-testing/Vagrantfile b/infrastructure/vagrant/Vagrantfile
similarity index 62%
rename from deployment/local-testing/Vagrantfile
rename to infrastructure/vagrant/Vagrantfile
index 349567b55a..17dabb39ed 100644
--- a/deployment/local-testing/Vagrantfile
+++ b/infrastructure/vagrant/Vagrantfile
@@ -1,11 +1,6 @@
-# Defines our Vagrant environment
-#
-# -*- mode: ruby -*-
-# vi: set ft=ruby :
-
Vagrant.configure("2") do |config|
- config.vm.box = 'generic/alpine319'
- config.vm.provision :shell, path: "bootstrap.sh"
+ config.vm.box = "packer_dreammall"
+ config.vm.box_url = "file://../packer-output/package.box"
config.vm.synced_folder '.', '/vagrant', disabled: true
config.vm.network "forwarded_port", guest: 80, host: 8000
config.vm.network "forwarded_port", guest: 8080, host: 8080