Browse Source

1st draft

master
Brennen Bearnes 5 years ago
parent
commit
ee206836b7
2 changed files with 1111 additions and 0 deletions
  1. +567
    -0
      README.md
  2. +544
    -0
      index.html

+ 567
- 0
README.md View File

@ -0,0 +1,567 @@
### Introduction
Parse is a Mobile Backend as a Service platform, owned by Facebook since 2013. In January of 2016, Parse [announced][our-incredible-journey] that its hosted services would shut down completely on January 28, 2017. In order to help its users transition away from the service, Parse has released an open source version of its backend, called **Parse Server**, which can be deployed to environments running Node.js and MongoDB.
This guide focuses on migrating a pre-existing Parse application to a standalone instance of Parse Server, with TLS/SSL encryption for all connections using a certificate provided by Let's Encrypt.
<$>[warning]
**Warning:** It is strongly recommended that this procedure first be tested with a development or test version of the app before attempting it with a user-facing production app.
<$>
## Prerequisites
This guide builds on [How To Run Parse Server on Ubuntu 14.04][run-parse-server]. It requires the following:
- An Ubuntu 14.04 server, configured with a non-root `sudo` user
- A recent Node.js release
- MongoDB 3.0.x
- A domain name pointing at the server
- A Parse App to be migrated
The target server should have enough storage to handle all of your app's data. Since Parse compresses data on their end, they officially recommend that you provision at least 10 times as much storage space as used by your hosted app.
## Step 1 – Install Let's Encrypt and Retrieve a Certificate
### Install Let's Encrypt and Dependencies
Let's Encrypt is a Certificate Authority that provides an easy way to obtain free TLS/SSL certificates. Because a certificate is necessary to secure both the migration of data to MongoDB and your Parse Server API endpoint, we'll begin by retrieving one with the `letsencrypt` client.
You must own or control the registered domain name that you wish to use the certificate with. If you do not already have a registered domain name, you may register one with one of the many domain name registrars out there (e.g. Namecheap, GoDaddy, etc.).
If you haven't already, be sure to create an **A Record** that points your domain to the public IP address of your server. This is required because of how Let's Encrypt validates that you own the domain it is issuing a certificate for. For example, if you want to obtain a certificate for `example.com`, that domain must resolve to your server for the validation process to work.
For more detail on this process, see [How To Set Up a Host Name with DigitalOcean][howto-hostname] and [How To Point to DigitalOcean Nameservers from Common Domain Registrars][howto-nameservers].
Begin by making sure that the `git` and `bc` packages are installed:
```command
sudo apt-get -y install git bc
```
Next, clone the `letsencrypt` repository from GitHub to `/opt/letsencrypt`. The `/opt/` directory is a standard location for software that's not installed from official packages:
```command
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
```
Change to the `letsencrypt` directory:
```command
cd /opt/letsencrypt
```
### Retrieve Initial Certificate
Run `letsencrypt` with the Standalone plugin:
```command
./letsencrypt-auto certonly --standalone
```
You'll be prompted to answer several questions, including your email address, agreement to a Terms of Service, and the domain name(s) for the certificate. Once finished, you'll receive notes much like the following:
```
IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/<^>your_domain_name<^>/fullchain.pem. Your cert will expire
on <^>2016-05-16<^>. To obtain a new version of the certificate in the
future, simply run Let's Encrypt again.
- If you like Let's Encrypt, please consider supporting our work by:
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le
```
Note the path and expiration date of your certificate, highlighted in the example output. Your certificate files should now be available in `/etc/letsencrypt/<^>your_domain_name<^>/`.
### Set Up Let's Encrypt Auto Renewal
<$>[warning]
**Warning:** You can safely complete this guide without worrying about certificate renewal, but you will need to address it for any long-lived production environment.
<$>
You may have noticed that your Let's Encrypt certificate is due to expire in 90 days. This is a deliberate feature of the Let's Encrypt approach, intended to minimize the amount of time that a compromised certificate can exist in the wild if something goes wrong.
Let's Encrypt is still in beta. Better auto-renewal features are planned, but in the meanwhile you will either have to repeat the certificate retrieval process by hand, or use a scheduled script to handle it for you. The details of automating this process are covered in [How To Secure Nginx with Let's Encrypt on Ubuntu 14.04][howto-letsencrypt], particularly the section on [setting up auto renewal][howto-letsencrypt-auto].
## Step 2 – Configure MongoDB for Migration
Parse provides a migration tool for existing applications. In order to make use of it, we need to open MongoDB to external connections and secure it with a copy of the TLS/SSL certificate from Let's Encrypt. Start by combining `fullchain1.pem` and `privkey1.pem` into a new file in `/etc/ssl`:
```command
sudo cat /etc/letsencrypt/archive/<^>domain_name<^>/{fullchain1.pem,privkey1.pem} | sudo tee /etc/ssl/mongo.pem
```
<$>[note]
You will have to repeat the above command after renewing your Let's Encrypt certificate. If you configure auto-renewal of the Let's Encrypt certificate, remember to include this operation.
<$>
Make sure `mongo.pem` is owned by the **mongodb** user, and readable only by its owner:
```command
sudo chown mongodb:mongodb /etc/ssl/mongo.pem
sudo chmod 600 /etc/ssl/mongo.pem
```
Now, open `/etc/mongod.conf` in `nano` (or your text editor of choice):
```command
sudo nano /etc/mongod.conf
```
Here, we'll make several important changes.
First, look for the `bindIp` line in the `net:` section, and tell MongoDB to listen on all addresses by changing `127.0.0.1` to `0.0.0.0`. Below this, add SSL configuration to the same section:
```
[label /etc/mongod.conf]
# network interfaces
net:
port: 27017
bindIp: <^>0.0.0.0<^>
<^>ssl:<^>
<^>mode: requireSSL<^>
<^>PEMKeyFile: /etc/ssl/mongo.pem<^>
```
Next, under `# security`, enable client authorization:
```
[label /etc/mongod.conf]
# security
security:
authorization: enabled
```
Finally, the migration tool requires us to set the `failIndexKeyTooLong` parameter to `false`:
```
[label /etc/mongod.conf]
setParameter:
failIndexKeyTooLong: false
```
<$>[note]
**Note:** Whitespace is significant in MongoDB configuration files, which are based on [YAML][yaml]. When copying configuration values, make sure that you preserve indentation.
<$>
Exit and save the file.
Before restarting the `mongod` service, we need to add a user with the `admin` role. Connect to the running MongoDB instance:
```command
mongo --port 27017
```
Create an admin user and exit. Be sure to replace <^>sammy<^> with your desired username and <^>password<^> with a strong password.
```
use admin
db.createUser({
user: "<^>sammy<^>",
pwd: "<^>password<^>",
roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
})
exit
```
Restart the `mongod` service:
```command
sudo service mongod restart
```
## Step 2 – Migrate Application Data from Parse
Now that you have a remotely-accessible MongoDB instance, you can use the Parse migration tool to transfer your app's data to your server.
### Configure MongoDB Credentials for Migration Tool
We'll begin by connecting locally with our new admin user:
```command
mongo --port 27017 --ssl --sslAllowInvalidCertificates --authenticationDatabase admin --username <^>sammy<^> --password
```
You should be prompted to enter the password you set earlier.
Once connected, choose a name for the database to store your app's data. For example, if you're migrating an app called Todo, you might use `todo`. You'll also need to pick another strong password for a user called **parse**.
From the `mongo` shell, give this user access to `<^>database_name<^>`:
```custom_prefix(>)
use <^>database_name<^>
db.createUser({ user: "parse", pwd: "<^>password<^>", roles: [ "readWrite", "dbAdmin" ] })
```
### Initiate Data Migration Process
In a browser window, log in to Parse, and open the settings for your app. Under **General**, locate the **Migrate** button and click it:
![Parse App Settings: General: Migrate](https://assets.digitalocean.com/articles/parse_migration/small-000.png)
You will be prompted for a MongoDB connection string. Use the following format:
```
mongodb://parse:<^>password<^>@<^>your_domain_name<^>:27017/<^>database_name<^>?ssl=true
```
For example, if you are using the domain `example.com`, with the user `parse`, the password `foo`, and a database called `todo`, your connection string would look like this:
```
mongodb://parse:foo@example.com:27017/todo?ssl=true
```
Don't forget `?ssl=true` at the end, or the connection will fail. Enter the connection string into the dialog like so:
![Parse App: Migration Dialog](https://assets.digitalocean.com/articles/parse_migration/small-001.png)
Click **Begin the migration**. You should see progress dialogs for copying a snapshot of your Parse hosted database to your server, and then for syncing new data since the snapshot was taken. The duration of this process will depend on the amount of data to be transferred, and may be substantial.
![Parse App: Migration Progress](https://assets.digitalocean.com/articles/parse_migration/small-002.png)
![Parse App: Migration Process](https://assets.digitalocean.com/articles/parse_migration/small-003.png)
### Verify Data Migration
Once finished, the migration process will enter a verification step. Don't finalize the migration yet. You'll first want to make sure the data has actually transferred, and test a local instance of Parse Server.
![Parse App: Finished Migration, Waiting for Finalization](https://assets.digitalocean.com/articles/parse_migration/small-004.png)
From your `mongo` shell, you can now examine your local database. Begin by using <^>database_name<^> and examining the collections it contains:
```custom_prefix(>)
use <^>database_name<^>
```
```custom_prefix(>)
show collections
```
```
[secondary_label Sample Output for Todo App]
Todo
_Index
_SCHEMA
_Session
_User
_dummy
system.indexes
```
You can examine the contents of a specific collection with the `.find()` method:
```custom_prefix(>)
db.<^>ApplicationName<^>.find()
```
```
[secondary_label Sample Output for Todo App]
> <^>db.Todo.find()<^>
{ "_id" : "hhbrhmBrs0", "order" : NumberLong(1), "_p_user" : "_User$dceklyR50A", "done" : false, "_acl" : { "dceklyR50A" : { "r" : true, "w" : true } }, "_rperm" : [ "dceklyR50A" ], "content" : "Migrate this app to my own server.", "_updated_at" : ISODate("2016-02-08T20:44:26.157Z"), "_wperm" : [ "dceklyR50A" ], "_created_at" : ISODate("2016-02-08T20:44:26.157Z") }
```
Your specific output will be different, but you should see data for your app. Once satisfied, exit `mongo` and return to the shell:
```custom_prefix(>)
exit
```
## Step 3 – Install and Configure Parse Server and PM2
### Create a Dedicated Parse User
Instead of running `parse-server` as **root** or your `sudo` user, we'll create a system user called **parse**:
```command
sudo useradd --create-home --system parse
```
Now set a password for **parse**:
```command
sudo passwd parse
```
You'll be prompted to enter a password twice.
### Install Parse Server and PM2 Globally
Use `npm` to install the `parse-server` utility, the `pm2` process manager, and their dependencies, globally:
```command
sudo npm install -g parse-server pm2
```
[PM2][pm2] is a feature-rich process manager, popular with Node.js developers. We'll use the `pm2` utility to run a simple wrapper script which configures our `parse-server` instance.
### Retrieve Keys and Write ecosystem.json
You'll need to retrieve some of the keys for your app. In the Parse dashboard, click on **App Settings** followed by **Security & Keys**:
![Parse Dashboard: App Settings: Security & Keys](https://assets.digitalocean.com/articles/parse_migration/small-007.png)
Of these, only the **Application ID** and **Master Key** are required. Others (client, JavaScript, .NET, and REST API keys) _may_ be necessary to support older client builds, but, if set, will be required in all requests. Unless you have reason to believe otherwise, you should begin by using just the Application ID and Master Key.
With these keys ready to hand, edit a new file called `/home/parse/ecosystem.json`:
```command
sudo nano /home/parse/ecosystem.json
```
Paste the following, changing configuration values to reflect your MongoDB connection string, Application ID, and Master Key:
```
{
"apps" : [{
"name" : "parse-wrapper",
"script" : "/usr/bin/parse-server",
"watch" : true,
"merge_logs" : true,
"cwd" : "/home/parse",
"env": {
// "PARSE_SERVER_CLOUD_CODE_MAIN": "/home/parse/cloud/main.js",
"PARSE_SERVER_DATABASE_URI": "mongodb://<^>parse<^>:<^>password<^>@<^>your_domain_name<^>:27017/<^>database_name<^>?ssl=true",
"PARSE_SERVER_APPLICATION_ID": "<^>your_application_id<^>",
"PARSE_SERVER_MASTER_KEY": "<^>your_master_key<^>",
}
}]
}
```
The `env` object is used to set environment variables. If you need to configure additional keys, `parse-server` also recognizes the following variables:
- `PARSE_SERVER_COLLECTION_PREFIX`
- `PARSE_SERVER_CLIENT_KEY`
- `PARSE_SERVER_REST_API_KEY`
- `PARSE_SERVER_DOTNET_KEY`
- `PARSE_SERVER_JAVASCRIPT_KEY`
- `PARSE_SERVER_DOTNET_KEY`
- `PARSE_SERVER_FILE_KEY`
- `PARSE_SERVER_FACEBOOK_APP_IDS`
Exit and save `ecosystem.json`.
Now, use the `su` command to become the **parse** user, and run the script with `pm2`:
```command
sudo su parse
```
```custom_prefix(parse\s$)
cd ~
pm2 start ecosystem.json
```
```
[secondary_label Sample Output]
...
[PM2] Spawning PM2 daemon
[PM2] PM2 Successfully daemonized
[PM2] Process launched
┌───────────────┬────┬──────┬──────┬────────┬─────────┬────────┬─────────────┬──────────┐
│ App name │ id │ mode │ pid │ status │ restart │ uptime │ memory │ watching │
├───────────────┼────┼──────┼──────┼────────┼─────────┼────────┼─────────────┼──────────┤
│ parse-wrapper │ 0 │ fork │ 3499 │ online │ 0 │ 0s │ 13.680 MB │ enabled │
└───────────────┴────┴──────┴──────┴────────┴─────────┴────────┴─────────────┴──────────┘
Use `pm2 show <id|name>` to get more details about an app
```
Now tell `pm2` to save this process list:
```custom_prefix(parse\s$)
pm2 save
```
```
[secondary_label Sample Output]
[PM2] Dumping processes
```
The list of processes `pm2` is running for the **parse** user should now be stored in `/home/parse/.pm2`. In order to restore the `parse-wrapper` we defined in `ecosystem.json` the next time the server restarts, we will just need to define a startup script to run `pm2` as **parse** and restore its processes. Fortunately, `pm2` can generate and install a script on its own.
Exit to your regular `sudo` user:
```custom_prefix(parse\s$)
exit
```
Tell `pm2` to install initialization scripts, to be run as the **parse** user, using `/home/parse` as a home directory:
```command
sudo pm2 startup ubuntu -u parse --hp /home/parse/
```
```
[label Output]
[PM2] Spawning PM2 daemon
[PM2] PM2 Successfully daemonized
[PM2] Generating system init script in /etc/init.d/pm2-init.sh
[PM2] Making script booting at startup...
[PM2] -ubuntu- Using the command:
su -c "chmod +x /etc/init.d/pm2-init.sh && update-rc.d pm2-init.sh defaults"
System start/stop links for /etc/init.d/pm2-init.sh already exist.
[PM2] Done.
```
## Step 4 – Install and Configure Nginx
We'll use the Nginx web server to provide a **reverse proxy** to `parse-server`, so that we can serve the Parse API securely over TLS/SSL.
Install the `nginx` package:
```command
sudo apt-get install -y nginx
```
Open `/etc/nginx/sites-enabled/default` in `nano` (or your editor of choice):
```command
sudo nano /etc/nginx/sites-enabled/default
```
Replace its contents with the following:
```
[label /etc/nginx/sites-enabled/default]
# HTTP - redirect all requests to HTTPS
server {
listen 80;
listen [::]:80 default_server ipv6only=on;
return 301 https://$host$request_uri;
}
# HTTPS - serve HTML from /usr/share/nginx/html, proxy requests to /parse/
# through to Parse Server
server {
listen 443;
server_name <^>your_domain_name<^>;
root /usr/share/nginx/html;
index index.html index.htm;
ssl on;
# Use certificate and key provided by Let's Encrypt:
ssl_certificate /etc/letsencrypt/live/<^>your_domain_name<^>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<^>your_domain_name<^>/privkey.pem;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
# Pass requests for /parse/ to Parse Server instance at localhost:1337
location /parse/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:1337/;
proxy_ssl_session_reuse off;
proxy_set_header Host $http_host;
proxy_redirect off;
}
location / {
try_files $uri $uri/ =404;
}
}
```
Exit the editor and save the file. Restart Nginx so that changes take effect:
```command
sudo service nginx restart
```
```
[secondary_label Output]
* Restarting nginx nginx
...done.
```
## Step 5 – Test Parse Server
At this stage, you should have the following:
- A TLS/SSL certificate, provided by Let's Encrypt
- MongoDB, secured with the Let's Encrypt certificate
- `parse-server` running under the **parse** user on port 1337, configured with the keys expected by your app
- `pm2` managing the `parse-server` process under the **parse** user, and a startup script to restart `pm2` on boot
- `nginx`, secured with the Let's Encrypt certificate, and configured to proxy connections to `https://<^>your_domain_name<^>/parse` to the `parse-server` instance
It should be possible to test reads and writes using `curl`.
<$>[note]
**Note:** The `curl` commands in this section should be harmless when used with a test or development app. Be cautious when writing data to a production app.
<$>
### Writing Data with a POST
You'll need to give `curl` several important options:
| Option | Description |
| ------ | ----------- |
| `-X POST` | Sets the request type, which would otherwise default to `GET` |
| `-H "X-Parse-Application-Id: <^>your_application_id<^>"` | Sends a header which identifies your application to `parse-server` |
| `-H "Content-Type: application/json"` | Sends a header which lets `parse-server` know to expect JSON-formatted data |
| `-d '{<^>json_data<^>}` | Sends the data itself |
Putting these all together, we get:
```
curl -X POST \
-H "X-Parse-Application-Id: <^>your_application_id<^>" \
-H "Content-Type: application/json" \
-d '{"score":1337,"playerName":"Sammy","cheatMode":false}' \
https://<^>your_domain_name<^>/parse/classes/GameScore
```
```
[label Sample Output]
{"objectId":"YpxFdzox3u","createdAt":"2016-02-18T18:03:43.188Z"}
```
### Reading Data with a GET
Since `curl` sends GET requests by default, and we're not supplying any data, you should only need to send the Application ID in order to read some sample data back:
```command
curl -H "X-Parse-Application-Id: <^>your_application_id<^>" https://p1k3.com/parse/classes/GameScore
```
```
[label Sample Output]
{"results":[{"objectId":"BNGLzgF6KB","score":1337,"playerName":"Sammy","cheatMode":false,"updatedAt":"2016-02-17T20:53:59.947Z","createdAt":"2016-02-17T20:53:59.947Z"},{"objectId":"0l1yE3ivB6","score":1337,"playerName":"Sean Plott","cheatMode":false,"updatedAt":"2016-02-18T03:57:00.932Z","createdAt":"2016-02-18T03:57:00.932Z"},{"objectId":"aKgvFqDkXh","score":1337,"playerName":"Sean Plott","cheatMode":false,"updatedAt":"2016-02-18T04:44:01.275Z","createdAt":"2016-02-18T04:44:01.275Z"},{"objectId":"zCKTgKzCRH","score":1337,"playerName":"Sean Plott","cheatMode":false,"updatedAt":"2016-02-18T16:56:51.245Z","createdAt":"2016-02-18T16:56:51.245Z"},{"objectId":"YpxFdzox3u","score":1337,"playerName":"Sean Plott","cheatMode":false,"updatedAt":"2016-02-18T18:03:43.188Z","createdAt":"2016-02-18T18:03:43.188Z"}]}
```
## Step 6 – Configure Your App for Parse Server
tk tk tk
## Step 7 – Finalize Migration
tk tk tk
![Parse Migration Finalization Dialog](https://assets.digitalocean.com/articles/parse_migration/small-005.png)
## Conclusion and Next Steps
tk tk tk
[env-vars]: https://www.digitalocean.com/community/tutorials/how-to-read-and-set-environmental-and-shell-variables-on-a-linux-vps
[howto-hostname]: https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean
[howto-letsencrypt]: https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04
[howto-letsencrypt-auto]: https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04#step-4-—-set-up-auto-renewal
[howto-mongodb]: https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-14-04
[howto-nameservers]: https://www.digitalocean.com/community/tutorials/how-to-point-to-digitalocean-nameservers-from-common-domain-registrars
[new-ubuntu-checklist]: https://www.digitalocean.com/community/tutorial_series/new-ubuntu-14-04-server-checklist
[nodesource]: https://github.com/nodesource/distributions
[nodesource-installation-instrux]: https://github.com/nodesource/distributions#installation-instructions
[parse-cloud-code]: https://parse.com/docs/cloudcode/guide
[parse-server-example]: https://github.com/ParsePlatform/parse-server-example.git
[parse-server-guide]: https://parse.com/docs/server/guide
[parse-server-guide-migrating]: https://parse.com/docs/server/guide#migrating
[parse-server]: https://github.com/ParsePlatform/parse-server
[run-parse-server]: https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04
[yaml]: http://yaml.org/
[pm2]: http://pm2.keymetrics.io/

+ 544
- 0
index.html View File

@ -0,0 +1,544 @@
<!DOCTYPE html>
<html lang=en>
<head>
<meta charset="utf-8">
<title>How To Migrate a Parse App to Parse Server on Ubuntu 14.04</title>
<link rel=stylesheet href="fake-it.css" />
</head>
<body>
<h3>Introduction</h3>
<p>Parse is a Mobile Backend as a Service platform, owned by Facebook since 2013. In January of 2016, Parse [announced][our-incredible-journey] that its hosted services would shut down completely on January 28, 2017. In order to help its users transition away from the service, Parse has released an open source version of its backend, called <strong>Parse Server</strong>, which can be deployed to environments running Node.js and MongoDB.</p>
<p>This guide focuses on migrating a pre-existing Parse application to a standalone instance of Parse Server, with TLS/SSL encryption for all connections using a certificate provided by Let&rsquo;s Encrypt.</p>
<p>&lt;$>[warning]
<strong>Warning:</strong> It is strongly recommended that this procedure first be tested with a development or test version of the app before attempting it with a user-facing production app.
&lt;$></p>
<h2>Prerequisites</h2>
<p>This guide builds on <a href="https://www.digitalocean.com/community/tutorials/how-to-run-parse-server-on-ubuntu-14-04">How To Run Parse Server on Ubuntu 14.04</a>. It requires the following:</p>
<ul>
<li>An Ubuntu 14.04 server, configured with a non-root <code>sudo</code> user</li>
<li>A recent Node.js release</li>
<li>MongoDB 3.0.x</li>
<li>A domain name pointing at the server</li>
<li>A Parse App to be migrated</li>
</ul>
<p>The target server should have enough storage to handle all of your app&rsquo;s data. Since Parse compresses data on their end, they officially recommend that you provision at least 10 times as much storage space as used by your hosted app.</p>
<h2>Step 1 – Install Let&rsquo;s Encrypt and Retrieve a Certificate</h2>
<h3>Install Let&rsquo;s Encrypt and Dependencies</h3>
<p>Let&rsquo;s Encrypt is a Certificate Authority that provides an easy way to obtain free TLS/SSL certificates. Because a certificate is necessary to secure both the migration of data to MongoDB and your Parse Server API endpoint, we&rsquo;ll begin by retrieving one with the <code>letsencrypt</code> client.</p>
<p>You must own or control the registered domain name that you wish to use the certificate with. If you do not already have a registered domain name, you may register one with one of the many domain name registrars out there (e.g. Namecheap, GoDaddy, etc.).</p>
<p>If you haven&rsquo;t already, be sure to create an <strong>A Record</strong> that points your domain to the public IP address of your server. This is required because of how Let&rsquo;s Encrypt validates that you own the domain it is issuing a certificate for. For example, if you want to obtain a certificate for <code>example.com</code>, that domain must resolve to your server for the validation process to work.</p>
<p>For more detail on this process, see <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean">How To Set Up a Host Name with DigitalOcean</a> and <a href="https://www.digitalocean.com/community/tutorials/how-to-point-to-digitalocean-nameservers-from-common-domain-registrars">How To Point to DigitalOcean Nameservers from Common Domain Registrars</a>.</p>
<p>Begin by making sure that the <code>git</code> and <code>bc</code> packages are installed:</p>
<pre><code class="command">sudo apt-get -y install git bc
</code></pre>
<p>Next, clone the <code>letsencrypt</code> repository from GitHub to <code>/opt/letsencrypt</code>. The <code>/opt/</code> directory is a standard location for software that&rsquo;s not installed from official packages:</p>
<pre><code class="command">sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
</code></pre>
<p>Change to the <code>letsencrypt</code> directory:</p>
<pre><code class="command">cd /opt/letsencrypt
</code></pre>
<h3>Retrieve Initial Certificate</h3>
<p>Run <code>letsencrypt</code> with the Standalone plugin:</p>
<pre><code class="command">./letsencrypt-auto certonly --standalone
</code></pre>
<p>You&rsquo;ll be prompted to answer several questions, including your email address, agreement to a Terms of Service, and the domain name(s) for the certificate. Once finished, you&rsquo;ll receive notes much like the following:</p>
<pre><code>IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/<span style="color: red;">your_domain_name</span>/fullchain.pem. Your cert will expire
on <span style="color: red;">2016-05-16</span>. To obtain a new version of the certificate in the
future, simply run Let's Encrypt again.
- If you like Let's Encrypt, please consider supporting our work by:
Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le
</code></pre>
<p>Note the path and expiration date of your certificate, highlighted in the example output. Your certificate files should now be available in <code>/etc/letsencrypt/<span style="color: red;">your_domain_name</span>/</code>.</p>
<h3>Set Up Let&rsquo;s Encrypt Auto Renewal</h3>
<p>&lt;$>[warning]
<strong>Warning:</strong> You can safely complete this guide without worrying about certificate renewal, but you will need to address it for any long-lived production environment.
&lt;$></p>
<p>You may have noticed that your Let&rsquo;s Encrypt certificate is due to expire in 90 days. This is a deliberate feature of the Let&rsquo;s Encrypt approach, intended to minimize the amount of time that a compromised certificate can exist in the wild if something goes wrong.</p>
<p>Let&rsquo;s Encrypt is still in beta. Better auto-renewal features are planned, but in the meanwhile you will either have to repeat the certificate retrieval process by hand, or use a scheduled script to handle it for you. The details of automating this process are covered in <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04">How To Secure Nginx with Let&rsquo;s Encrypt on Ubuntu 14.04</a>, particularly the section on <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04#step-4-%E2%80%94-set-up-auto-renewal">setting up auto renewal</a>.</p>
<h2>Step 2 – Configure MongoDB for Migration</h2>
<p>Parse provides a migration tool for existing applications. In order to make use of it, we need to open MongoDB to external connections and secure it with a copy of the TLS/SSL certificate from Let&rsquo;s Encrypt. Start by combining <code>fullchain1.pem</code> and <code>privkey1.pem</code> into a new file in <code>/etc/ssl</code>:</p>
<pre><code class="command">sudo cat /etc/letsencrypt/archive/<span style="color: red;">domain_name</span>/{fullchain1.pem,privkey1.pem} | sudo tee /etc/ssl/mongo.pem
</code></pre>
<p>&lt;$>[note]
You will have to repeat the above command after renewing your Let&rsquo;s Encrypt certificate. If you configure auto-renewal of the Let&rsquo;s Encrypt certificate, remember to include this operation.
&lt;$></p>
<p>Make sure <code>mongo.pem</code> is owned by the <strong>mongodb</strong> user, and readable only by its owner:</p>
<pre><code class="command">sudo chown mongodb:mongodb /etc/ssl/mongo.pem
sudo chmod 600 /etc/ssl/mongo.pem
</code></pre>
<p>Now, open <code>/etc/mongod.conf</code> in <code>nano</code> (or your text editor of choice):</p>
<pre><code class="command">sudo nano /etc/mongod.conf
</code></pre>
<p>Here, we&rsquo;ll make several important changes.</p>
<p>First, look for the <code>bindIp</code> line in the <code>net:</code> section, and tell MongoDB to listen on all addresses by changing <code>127.0.0.1</code> to <code>0.0.0.0</code>. Below this, add SSL configuration to the same section:</p>
<pre><code><strong>/etc/mongod.conf</strong><br>
# network interfaces
net:
port: 27017
bindIp: <span style="color: red;">0.0.0.0</span>
<span style="color: red;">ssl:</span>
<span style="color: red;">mode: requireSSL</span>
<span style="color: red;">PEMKeyFile: /etc/ssl/mongo.pem</span>
</code></pre>
<p>Next, under <code># security</code>, enable client authorization:</p>
<pre><code><strong>/etc/mongod.conf</strong><br>
# security
security:
authorization: enabled
</code></pre>
<p>Finally, the migration tool requires us to set the <code>failIndexKeyTooLong</code> parameter to <code>false</code>:</p>
<pre><code><strong>/etc/mongod.conf</strong><br>
setParameter:
failIndexKeyTooLong: false
</code></pre>
<p>&lt;$>[note]
<strong>Note:</strong> Whitespace is significant in MongoDB configuration files, which are based on <a href="http://yaml.org/">YAML</a>. When copying configuration values, make sure that you preserve indentation.
&lt;$></p>
<p>Exit and save the file.</p>
<p>Before restarting the <code>mongod</code> service, we need to add a user with the <code>admin</code> role. Connect to the running MongoDB instance:</p>
<pre><code class="command">mongo --port 27017
</code></pre>
<p>Create an admin user and exit. Be sure to replace &lt;^>sammy&lt;^> with your desired username and &lt;^>password&lt;^> with a strong password.</p>
<pre><code>use admin
db.createUser({
user: "<span style="color: red;">sammy</span>",
pwd: "<span style="color: red;">password</span>",
roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
})
exit
</code></pre>
<p>Restart the <code>mongod</code> service:</p>
<pre><code class="command">sudo service mongod restart
</code></pre>
<h2>Step 2 – Migrate Application Data from Parse</h2>
<p>Now that you have a remotely-accessible MongoDB instance, you can use the Parse migration tool to transfer your app&rsquo;s data to your server.</p>
<h3>Configure MongoDB Credentials for Migration Tool</h3>
<p>We&rsquo;ll begin by connecting locally with our new admin user:</p>
<pre><code class="command">mongo --port 27017 --ssl --sslAllowInvalidCertificates --authenticationDatabase admin --username <span style="color: red;">sammy</span> --password
</code></pre>
<p>You should be prompted to enter the password you set earlier.</p>
<p>Once connected, choose a name for the database to store your app&rsquo;s data. For example, if you&rsquo;re migrating an app called Todo, you might use <code>todo</code>. You&rsquo;ll also need to pick another strong password for a user called <strong>parse</strong>.</p>
<p>From the <code>mongo</code> shell, give this user access to <code><span style="color: red;">database_name</span></code>:</p>
<pre><code class="custom_prefix(>)">use <span style="color: red;">database_name</span>
db.createUser({ user: "parse", pwd: "<span style="color: red;">password</span>", roles: [ "readWrite", "dbAdmin" ] })
</code></pre>
<h3>Initiate Data Migration Process</h3>
<p>In a browser window, log in to Parse, and open the settings for your app. Under <strong>General</strong>, locate the <strong>Migrate</strong> button and click it:</p>
<p><img src="https://assets.digitalocean.com/articles/parse_migration/small-000.png" alt="Parse App Settings: General: Migrate" /></p>
<p>You will be prompted for a MongoDB connection string. Use the following format:</p>
<pre><code>mongodb://parse:<span style="color: red;">password</span>@<span style="color: red;">your_domain_name</span>:27017/<span style="color: red;">database_name</span>?ssl=true
</code></pre>
<p>For example, if you are using the domain <code>example.com</code>, with the user <code>parse</code>, the password <code>foo</code>, and a database called <code>todo</code>, your connection string would look like this:</p>
<pre><code>mongodb://parse:foo@example.com:27017/todo?ssl=true
</code></pre>
<p>Don&rsquo;t forget <code>?ssl=true</code> at the end, or the connection will fail. Enter the connection string into the dialog like so:</p>
<p><img src="https://assets.digitalocean.com/articles/parse_migration/small-001.png" alt="Parse App: Migration Dialog" /></p>
<p>Click <strong>Begin the migration</strong>. You should see progress dialogs for copying a snapshot of your Parse hosted database to your server, and then for syncing new data since the snapshot was taken. The duration of this process will depend on the amount of data to be transferred, and may be substantial.</p>
<p><img src="https://assets.digitalocean.com/articles/parse_migration/small-002.png" alt="Parse App: Migration Progress" /></p>
<p><img src="https://assets.digitalocean.com/articles/parse_migration/small-003.png" alt="Parse App: Migration Process" /></p>
<h3>Verify Data Migration</h3>
<p>Once finished, the migration process will enter a verification step. Don&rsquo;t finalize the migration yet. You&rsquo;ll first want to make sure the data has actually transferred, and test a local instance of Parse Server.</p>
<p><img src="https://assets.digitalocean.com/articles/parse_migration/small-004.png" alt="Parse App: Finished Migration, Waiting for Finalization" /></p>
<p>From your <code>mongo</code> shell, you can now examine your local database. Begin by using &lt;^>database_name&lt;^> and examining the collections it contains:</p>
<pre><code class="custom_prefix(>)">use <span style="color: red;">database_name</span>
</code></pre>
<pre><code class="custom_prefix(>)">show collections
</code></pre>
<pre><code><span style="color: gray;">Sample Output for Todo App</span><br>
Todo
_Index
_SCHEMA
_Session
_User
_dummy
system.indexes
</code></pre>
<p>You can examine the contents of a specific collection with the <code>.find()</code> method:</p>
<pre><code class="custom_prefix(>)">db.<span style="color: red;">ApplicationName</span>.find()
</code></pre>
<pre><code><span style="color: gray;">Sample Output for Todo App</span><br>
&gt; <span style="color: red;">db.Todo.find()</span>
{ "_id" : "hhbrhmBrs0", "order" : NumberLong(1), "_p_user" : "_User$dceklyR50A", "done" : false, "_acl" : { "dceklyR50A" : { "r" : true, "w" : true } }, "_rperm" : [ "dceklyR50A" ], "content" : "Migrate this app to my own server.", "_updated_at" : ISODate("2016-02-08T20:44:26.157Z"), "_wperm" : [ "dceklyR50A" ], "_created_at" : ISODate("2016-02-08T20:44:26.157Z") }
</code></pre>
<p>Your specific output will be different, but you should see data for your app. Once satisfied, exit <code>mongo</code> and return to the shell:</p>
<pre><code class="custom_prefix(>)">exit
</code></pre>
<h2>Step 3 – Install and Configure Parse Server and PM2</h2>
<h3>Create a Dedicated Parse User</h3>
<p>Instead of running <code>parse-server</code> as <strong>root</strong> or your <code>sudo</code> user, we&rsquo;ll create a system user called <strong>parse</strong>:</p>
<pre><code class="command">sudo useradd --create-home --system parse
</code></pre>
<p>Now set a password for <strong>parse</strong>:</p>
<pre><code class="command">sudo passwd parse
</code></pre>
<p>You&rsquo;ll be prompted to enter a password twice.</p>
<h3>Install Parse Server and PM2 Globally</h3>
<p>Use <code>npm</code> to install the <code>parse-server</code> utility, the <code>pm2</code> process manager, and their dependencies, globally:</p>
<pre><code class="command">sudo npm install -g parse-server pm2
</code></pre>
<p><a href="http://pm2.keymetrics.io/">PM2</a> is a feature-rich process manager, popular with Node.js developers. We&rsquo;ll use the <code>pm2</code> utility to run a simple wrapper script which configures our <code>parse-server</code> instance.</p>
<h3>Retrieve Keys and Write ecosystem.json</h3>
<p>You&rsquo;ll need to retrieve some of the keys for your app. In the Parse dashboard, click on <strong>App Settings</strong> followed by <strong>Security &amp; Keys</strong>:</p>
<p><img src="https://assets.digitalocean.com/articles/parse_migration/small-007.png" alt="Parse Dashboard: App Settings: Security &amp; Keys" /></p>
<p>Of these, only the <strong>Application ID</strong> and <strong>Master Key</strong> are required. Others (client, JavaScript, .NET, and REST API keys) <em>may</em> be necessary to support older client builds, but, if set, will be required in all requests. Unless you have reason to believe otherwise, you should begin by using just the Application ID and Master Key.</p>
<p>With these keys ready to hand, edit a new file called <code>/home/parse/ecosystem.json</code>:</p>
<pre><code class="command">sudo nano /home/parse/ecosystem.json
</code></pre>
<p>Paste the following, changing configuration values to reflect your MongoDB connection string, Application ID, and Master Key:</p>
<pre><code>{
"apps" : [{
"name" : "parse-wrapper",
"script" : "/usr/bin/parse-server",
"watch" : true,
"merge_logs" : true,
"cwd" : "/home/parse",
"env": {
// "PARSE_SERVER_CLOUD_CODE_MAIN": "/home/parse/cloud/main.js",
"PARSE_SERVER_DATABASE_URI": "mongodb://<span style="color: red;">parse</span>:<span style="color: red;">password</span>@<span style="color: red;">your_domain_name</span>:27017/<span style="color: red;">database_name</span>?ssl=true",
"PARSE_SERVER_APPLICATION_ID": "<span style="color: red;">your_application_id</span>",
"PARSE_SERVER_MASTER_KEY": "<span style="color: red;">your_master_key</span>",
}
}]
}
</code></pre>
<p>The <code>env</code> object is used to set environment variables. If you need to configure additional keys, <code>parse-server</code> also recognizes the following variables:</p>
<ul>
<li><code>PARSE_SERVER_COLLECTION_PREFIX</code></li>
<li><code>PARSE_SERVER_CLIENT_KEY</code></li>
<li><code>PARSE_SERVER_REST_API_KEY</code></li>
<li><code>PARSE_SERVER_DOTNET_KEY</code></li>
<li><code>PARSE_SERVER_JAVASCRIPT_KEY</code></li>
<li><code>PARSE_SERVER_DOTNET_KEY</code></li>
<li><code>PARSE_SERVER_FILE_KEY</code></li>
<li><code>PARSE_SERVER_FACEBOOK_APP_IDS</code></li>
</ul>
<p>Exit and save <code>ecosystem.json</code>.</p>
<p>Now, use the <code>su</code> command to become the <strong>parse</strong> user, and run the script with <code>pm2</code>:</p>
<pre><code class="command">sudo su parse
</code></pre>
<pre><code class="custom_prefix(parse\s$)">cd ~
pm2 start ecosystem.json
</code></pre>
<pre><code><span style="color: gray;">Sample Output</span><br>
...
[PM2] Spawning PM2 daemon
[PM2] PM2 Successfully daemonized
[PM2] Process launched
┌───────────────┬────┬──────┬──────┬────────┬─────────┬────────┬─────────────┬──────────┐
│ App name │ id │ mode │ pid │ status │ restart │ uptime │ memory │ watching │
├───────────────┼────┼──────┼──────┼────────┼─────────┼────────┼─────────────┼──────────┤
│ parse-wrapper │ 0 │ fork │ 3499 │ online │ 0 │ 0s │ 13.680 MB │ enabled │
└───────────────┴────┴──────┴──────┴────────┴─────────┴────────┴─────────────┴──────────┘
Use `pm2 show &lt;id|name&gt;` to get more details about an app
</code></pre>
<p>Now tell <code>pm2</code> to save this process list:</p>
<pre><code class="custom_prefix(parse\s$)">pm2 save
</code></pre>
<pre><code><span style="color: gray;">Sample Output</span><br>
[PM2] Dumping processes
</code></pre>
<p>The list of processes <code>pm2</code> is running for the <strong>parse</strong> user should now be stored in <code>/home/parse/.pm2</code>. In order to restore the <code>parse-wrapper</code> we defined in <code>ecosystem.json</code> the next time the server restarts, we will just need to define a startup script to run <code>pm2</code> as <strong>parse</strong> and restore its processes. Fortunately, <code>pm2</code> can generate and install a script on its own.</p>
<p>Exit to your regular <code>sudo</code> user:</p>
<pre><code class="custom_prefix(parse\s$)">exit
</code></pre>
<p>Tell <code>pm2</code> to install initialization scripts, to be run as the <strong>parse</strong> user, using <code>/home/parse</code> as a home directory:</p>
<pre><code class="command">sudo pm2 startup ubuntu -u parse --hp /home/parse/
</code></pre>
<pre><code><strong>Output</strong><br>
[PM2] Spawning PM2 daemon
[PM2] PM2 Successfully daemonized
[PM2] Generating system init script in /etc/init.d/pm2-init.sh
[PM2] Making script booting at startup...
[PM2] -ubuntu- Using the command:
su -c "chmod +x /etc/init.d/pm2-init.sh &amp;&amp; update-rc.d pm2-init.sh defaults"
System start/stop links for /etc/init.d/pm2-init.sh already exist.
[PM2] Done.
</code></pre>
<h2>Step 4 – Install and Configure Nginx</h2>
<p>We&rsquo;ll use the Nginx web server to provide a <strong>reverse proxy</strong> to <code>parse-server</code>, so that we can serve the Parse API securely over TLS/SSL.</p>
<p>Install the <code>nginx</code> package:</p>
<pre><code class="command">sudo apt-get install -y nginx
</code></pre>
<p>Open <code>/etc/nginx/sites-enabled/default</code> in <code>nano</code> (or your editor of choice):</p>
<pre><code class="command">sudo nano /etc/nginx/sites-enabled/default
</code></pre>
<p>Replace its contents with the following:</p>
<pre><code><strong>/etc/nginx/sites-enabled/default</strong><br>
# HTTP - redirect all requests to HTTPS
server {
listen 80;
listen [::]:80 default_server ipv6only=on;
return 301 https://$host$request_uri;
}
# HTTPS - serve HTML from /usr/share/nginx/html, proxy requests to /parse/
# through to Parse Server
server {
listen 443;
server_name <span style="color: red;">your_domain_name</span>;
root /usr/share/nginx/html;
index index.html index.htm;
ssl on;
# Use certificate and key provided by Let's Encrypt:
ssl_certificate /etc/letsencrypt/live/<span style="color: red;">your_domain_name</span>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<span style="color: red;">your_domain_name</span>/privkey.pem;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
# Pass requests for /parse/ to Parse Server instance at localhost:1337
location /parse/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:1337/;
proxy_ssl_session_reuse off;
proxy_set_header Host $http_host;
proxy_redirect off;
}
location / {
try_files $uri $uri/ =404;
}
}
</code></pre>
<p>Exit the editor and save the file. Restart Nginx so that changes take effect:</p>
<pre><code class="command">sudo service nginx restart
</code></pre>
<pre><code><span style="color: gray;">Output</span><br>
* Restarting nginx nginx
...done.
</code></pre>
<h2>Step 5 – Test Parse Server</h2>
<p>At this stage, you should have the following:</p>
<ul>
<li>A TLS/SSL certificate, provided by Let&rsquo;s Encrypt</li>
<li>MongoDB, secured with the Let&rsquo;s Encrypt certificate</li>
<li><code>parse-server</code> running under the <strong>parse</strong> user on port 1337, configured with the keys expected by your app</li>
<li><code>pm2</code> managing the <code>parse-server</code> process under the <strong>parse</strong> user, and a startup script to restart <code>pm2</code> on boot</li>
<li><code>nginx</code>, secured with the Let&rsquo;s Encrypt certificate, and configured to proxy connections to <code>https://<span style="color: red;">your_domain_name</span>/parse</code> to the <code>parse-server</code> instance</li>
</ul>
<p>It should be possible to test reads and writes using <code>curl</code>.</p>
<p>&lt;$>[note]
<strong>Note:</strong> The <code>curl</code> commands in this section should be harmless when used with a test or development app. Be cautious when writing data to a production app.
&lt;$></p>
<h3>Writing Data with a POST</h3>
<p>You&rsquo;ll need to give <code>curl</code> several important options:</p>
<table>
<thead>
<tr>
<th> Option </th>
<th> Description </th>
</tr>
</thead>
<tbody>
<tr>
<td> <code>-X POST</code> </td>
<td> Sets the request type, which would otherwise default to <code>GET</code> </td>
</tr>
<tr>
<td> <code>-H "X-Parse-Application-Id: <span style="color: red;">your_application_id</span>"</code> </td>
<td> Sends a header which identifies your application to <code>parse-server</code> </td>
</tr>
<tr>
<td> <code>-H "Content-Type: application/json"</code> </td>
<td> Sends a header which lets <code>parse-server</code> know to expect JSON-formatted data </td>
</tr>
<tr>
<td> <code>-d '{<span style="color: red;">json_data</span>}</code> </td>
<td> Sends the data itself </td>
</tr>
</tbody>
</table>
<p>Putting these all together, we get:</p>
<pre><code>curl -X POST \
-H "X-Parse-Application-Id: <span style="color: red;">your_application_id</span>" \
-H "Content-Type: application/json" \
-d '{"score":1337,"playerName":"Sammy","cheatMode":false}' \
https://<span style="color: red;">your_domain_name</span>/parse/classes/GameScore
</code></pre>
<pre><code><strong>Sample Output</strong><br>
{"objectId":"YpxFdzox3u","createdAt":"2016-02-18T18:03:43.188Z"}
</code></pre>
<h3>Reading Data with a GET</h3>
<p>Since <code>curl</code> sends GET requests by default, and we&rsquo;re not supplying any data, you should only need to send the Application ID in order to read some sample data back:</p>
<pre><code class="command">curl -H "X-Parse-Application-Id: <span style="color: red;">your_application_id</span>" https://p1k3.com/parse/classes/GameScore
</code></pre>
<pre><code><strong>Sample Output</strong><br>
{"results":[{"objectId":"BNGLzgF6KB","score":1337,"playerName":"Sammy","cheatMode":false,"updatedAt":"2016-02-17T20:53:59.947Z","createdAt":"2016-02-17T20:53:59.947Z"},{"objectId":"0l1yE3ivB6","score":1337,"playerName":"Sean Plott","cheatMode":false,"updatedAt":"2016-02-18T03:57:00.932Z","createdAt":"2016-02-18T03:57:00.932Z"},{"objectId":"aKgvFqDkXh","score":1337,"playerName":"Sean Plott","cheatMode":false,"updatedAt":"2016-02-18T04:44:01.275Z","createdAt":"2016-02-18T04:44:01.275Z"},{"objectId":"zCKTgKzCRH","score":1337,"playerName":"Sean Plott","cheatMode":false,"updatedAt":"2016-02-18T16:56:51.245Z","createdAt":"2016-02-18T16:56:51.245Z"},{"objectId":"YpxFdzox3u","score":1337,"playerName":"Sean Plott","cheatMode":false,"updatedAt":"2016-02-18T18:03:43.188Z","createdAt":"2016-02-18T18:03:43.188Z"}]}
</code></pre>
<h2>Step 6 – Configure Your App for Parse Server</h2>
<p>tk tk tk</p>
<h2>Step 7 – Finalize Migration</h2>
<p>tk tk tk</p>
<p><img src="https://assets.digitalocean.com/articles/parse_migration/small-005.png" alt="Parse Migration Finalization Dialog" /></p>
<h2>Conclusion and Next Steps</h2>
<p>tk tk tk</p>
</body>
</html>

Loading…
Cancel
Save